[
  {
    "path": ".github/workflows/on-push.yml",
    "content": "name: On push (tests, build)\non: push\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v2\n        with:\n          node-version: '14.16'\n          registry-url: 'https://registry.npmjs.org'\n      - run: npm i\n      - run: npm test\n\n  build:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [macos-latest, ubuntu-latest, windows-latest]\n\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v2\n        with:\n          node-version: '14.16'\n          registry-url: 'https://registry.npmjs.org'\n      - name: Build/release Electron app\n        uses: samuelmeuli/action-electron-builder@v1\n        with:\n          # GitHub token, automatically provided to the action (no need to define this secret in the repo settings)\n          github_token: ${{ secrets.github_token }}\n\n          # this action will not release\n          release: false\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n\n.idea/\n*.iml\n\ndist/\nrelease/\n"
  },
  {
    "path": ".huskyrc",
    "content": "{\n  \"hooks\": {\n    \"pre-commit\": \"npm run lint && npm test\"\n  }\n}\n"
  },
  {
    "path": ".nvmrc",
    "content": "14.16\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n    \"singleQuote\": true,\n    \"jsxSingleQuote\": true,\n    \"arrowParens\": \"avoid\",\n    \"printWidth\": 160,\n    \"tabWidth\": 4,\n    \"semi\": false,\n    \"trailingComma\": \"none\",\n    \"bracketSpacing\": true\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"node\",\n            \"request\": \"launch\",\n            \"name\": \"Launch Program\",\n            \"program\": \"${workspaceFolder}/dist/main.js\"\n        }\n    ]\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 codecentric AG\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Gopass UI [![Latest release](https://img.shields.io/github/release/codecentric/gopass-ui.svg)](https://github.com/codecentric/gopass-ui/releases/latest)\n\n<img src=\"docs/img/gopass-ui-logo.png\" alt=\"Gopass UI logo\" style=\"max-width: 800px\">\n\n## What is Gopass and Gopass UI?\n\n> [Gopass](https://github.com/gopasspw/gopass) is a rewrite of the pass password manager in Go with the aim of making it cross-platform and adding additional features – (Taken from Github)\n\n`Gopass UI` is wrapping `gopass` from your command line. It makes your life easier by providing a graphical user interface to search and manage your secrets. It looks like this:\n\n<img src=\"docs/img/demo-720p.gif\" alt=\"GIF demonstrating core features of Gopass UI\" title=\"Gopass UI demo\" style=\"max-width: 720px\" />\n\nIn addition there is a search window that can be opened with `(cmd || ctrl) + shift + p`.\n\n## How can I use it?\n\nFor most platforms just [download the latest packaged application](https://github.com/codecentric/gopass-ui/releases/latest) from our releases and install it. We provide binaries for MacOS, Windows and Linux (deb, RPM, snap, AppImage, Gentoo). For more details see [supported platforms and packages](docs/platforms-and-packages.md).\n\nOf course, you need to have [Gopass](https://github.com/gopasspw/gopass) up and running. We also recommend to use a dialog-based PIN-entry-tool for typing in GPG passphrases like [pinentry-mac for MacOS](https://formulae.brew.sh/formula/pinentry-mac) instead of using the default terminal-based.\n\n### Platform notice\n\nWe'll only test the MacOS builds and **are not able to offer support for Linux and Windows releases**. We are happy to review your pull requests addressing any of such issues.  \n\n## Issues and Contribution\n\nFeel free to report any usage issue. We are very keen about your feedback and appreciate any help.\nYou'd like to help us developing Gopass UI? Awesome! We are looking forward to your pull requests, issues and participation in discussion.\n\n## Development\n\nSee how to get started in our [development documentation](docs/development.md).\n"
  },
  {
    "path": "docs/development.md",
    "content": "## Development\n\n### Clone and install dependencies\n\nFirst, clone the repository and navigate inside:\n\n```bash\ngit clone https://github.com/codecentric/gopass-ui.git && cd gopass-ui/\n```\n\nThen, install the dependencies:\n\n```bash\nnvm use # make sure that nvm is installed on your machine and it installs the requested Node version\nnpm install\n```\n\n### Development\n\nThe app is divided into one main process and two renderer processes. One renderer process is for the global search window, the other one for the main explorer window.\nAll processes have to be started **simultaneously** in different console tabs:\n\n```bash\n# don't forget nvm use && npm install from the previous section ;-)\n\n# run this in a pane for powering the main process (the \"backend\")\nnpm run start-main-dev\n # run this in a pane for the renderer of the main/explorer window\nnpm run start-renderer-explorer-dev\n# run this in a pane for the renderer of the search window\nnpm run start-renderer-search-dev\n```\n\nThis will start the application with hot-reloading so you can instantly start developing and see the changes in the open application.\n\n### Testing\n\nWe use Jest for tests. Currently the project contains (too less) unit and integration tests. Unit tests should have no dependency to the local machine except the Node environment we're setting up. Integration tests can also involve system binaries like Gopass, GPG and so on – you got the point ;-)\n\nRun them with `npm test` and `npm run test:integration`.\n\n\n### Linting\n\nThis project contains `prettier` and `tslint`. **TLDR:** Prettier assists during development. Tslint is ran in a Husky pre-commit hook together with unit tests and in Github actions (see `.github/workflows/`).\n\n**Prettier** is more aggressive because it is designed opinionated. It will find and correct more . The only options we decide on are to be found in `.prettierrc`. We use Prettier to enforce and apply code style during development process. Right after saving an edited file it will correct code style mistakes! In VSCode this comes with the Prettier extension already. In JetBrains IntelliJ IDEA/Webstorm [this can be easily configured](https://prettier.io/docs/en/webstorm.html). On the CLI, feel free to use `npm run prettier:check` and `npm run prettier:write`.\n\n**Tslint** is used to check the code style baseline before commiting staged code and while running CI. Feel free to use the scripts `npm run lint` for linting and `npm run lint:fix` for fixing simple issues automatically to make code comply to the baseline style.\n\n\n### Production packaging\n\nWe use [Electron builder](https://www.electron.build/) to build and package the application. See how we use it to package all targeted platforms and formats in [our release docs](./releasing.md).\n\nPackaging will create all results in `releases` folder.\n"
  },
  {
    "path": "docs/platforms-and-packages.md",
    "content": "## Supported Platforms\n\nGopass-ui is available for the following platforms:\n* MacOS (.dmg)\n* Windows (.exe)\n* Linux, see next section\n\n### Linux Packages\n\nFor Linux the following packages are provided:\n* .deb (download [here](https://github.com/codecentric/gopass-ui/releases/latest))\n* .rpm (download [here](https://github.com/codecentric/gopass-ui/releases/latest))\n* .snap (download [here](https://github.com/codecentric/gopass-ui/releases/latest))\n* .pacman (download [here](https://github.com/codecentric/gopass-ui/releases/latest))\n* .AppImage (download [here](https://github.com/codecentric/gopass-ui/releases/latest))\n* Gentoo: `emerge app-admin/gopass-ui` ([gentoo overlay](https://gitlab.awesome-it.de/overlays/awesome), thanks [@danielcb](https://github.com/danielcb))\n"
  },
  {
    "path": "docs/releasing.md",
    "content": "## Releasing and Publishing Gopass UI\n\nThis documents the steps it needs to release and publish a new version of Gopass UI to Github.\n\n### In the codebase\n\n1. Let's check if code style and tests are okay: `npm run release:check`. If there are issues, fix them first.\n2. Increment the version number in `package.json` and do `npm i` to reflect it within `package-lock.json`.\n3. Build the releases for your local platform to verify everything is working: `npm run release`. This takes a while. If successful, the binaries were built in `release/`\n4. Build the releases for [all supported platforms and packages](./platforms-and-packages.md): `npm run release:full`\n5. As we know that everything worked, commit and push the version change\n\n### Draft and public release on Github\n\n1. [Draft a new release](https://github.com/codecentric/gopass-ui/releases/new)\n2. Choose the created Git tag.\n3. Write a precise but catchy release title. Maybe something about the core topics of this release etc.\n4. Describe this release in detail. What features were added or changed? Were bugs fixed? New platforms supported?\n5. Attach all binaries for this release from the `release/` directory.\n6. Publish and spread the word! 🎉🎉🎉\n"
  },
  {
    "path": "mocks/fileMock.js",
    "content": "module.exports = 'test-file-stub'\n"
  },
  {
    "path": "mocks/styleMock.js",
    "content": "module.exports = {}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"gopass-ui\",\n  \"version\": \"0.8.0\",\n  \"description\": \"Awesome UI for the gopass CLI – a password manager for your daily business\",\n  \"main\": \"./dist/main.js\",\n  \"scripts\": {\n    \"build-main\": \"cross-env NODE_ENV=production webpack --config webpack.main.prod.config.js\",\n    \"build-renderer-search\": \"cross-env NODE_ENV=production webpack --config webpack.renderer.search.prod.config.js\",\n    \"build-renderer-explorer\": \"cross-env NODE_ENV=production webpack --config webpack.renderer.explorer.prod.config.js\",\n    \"build\": \"npm run build-main && npm run build-renderer-explorer && npm run build-renderer-search\",\n    \"start-renderer-search-dev\": \"NODE_OPTIONS=\\\"--max-old-space-size=2048\\\" webpack-dev-server --config webpack.renderer.search.dev.config.js\",\n    \"start-renderer-explorer-dev\": \"NODE_OPTIONS=\\\"--max-old-space-size=2048\\\" webpack-dev-server --config webpack.renderer.explorer.dev.config.js\",\n    \"start-main-dev\": \"webpack --config webpack.main.config.js && electron ./dist/main.js\",\n    \"prestart\": \"npm run build\",\n    \"start\": \"electron .\",\n    \"prettier:check\": \"prettier --check '{src,test,mocks}/**/*.{ts,tsx,js,jsx}'\",\n    \"prettier:write\": \"prettier --write '{src,test,mocks}/**/*.{ts,tsx,js,jsx}'\",\n    \"lint\": \"tslint '{src,test,mocks}/**/*.{ts,tsx,js,jsx}' --project ./tsconfig.json\",\n    \"lint:fix\": \"tslint '{src,test,mocks}/**/*.{ts,tsx}' --project ./tsconfig.json --fix\",\n    \"test\": \"npm run test:unit\",\n    \"test:unit\": \"jest --testRegex '\\\\.test\\\\.tsx?$'\",\n    \"test:unit:watch\": \"jest --testRegex '\\\\.test\\\\.tsx?$' --watch\",\n    \"test:integration\": \"jest --testRegex '\\\\.itest\\\\.ts$'\",\n    \"release:check\": \"npm run lint && npm test\",\n    \"release\": \"npm run release:check && npm run build && electron-builder --publish onTag\",\n    \"release:full\": \"npm run release:check && npm run build && electron-builder --mac dmg --win --linux deb rpm snap AppImage pacman\",\n    \"postinstall\": \"electron-builder install-app-deps\"\n  },\n  \"jest\": {\n    \"transform\": {\n      \"^.+\\\\.tsx?$\": \"ts-jest\"\n    },\n    \"testRegex\": \"\\\\.?test\\\\.tsx?$\",\n    \"moduleFileExtensions\": [\n      \"ts\",\n      \"tsx\",\n      \"js\",\n      \"json\",\n      \"node\"\n    ],\n    \"moduleNameMapper\": {\n      \"\\\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$\": \"<rootDir>/mocks/fileMock.js\",\n      \"\\\\.(s?css|sass)$\": \"<rootDir>/mocks/styleMock.js\",\n      \"^electron$\": \"<rootDir>/test/mock/electron-mock.ts\"\n    }\n  },\n  \"build\": {\n    \"productName\": \"Gopass UI\",\n    \"appId\": \"de.codecentric.gopassui\",\n    \"directories\": {\n      \"output\": \"release\"\n    },\n    \"files\": [\n      \"dist/\",\n      \"node_modules/\",\n      \"package.json\"\n    ],\n    \"mac\": {\n      \"category\": \"public.app-category.productivity\"\n    },\n    \"publish\": [\n      \"github\"\n    ]\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+ssh://git@github.com:codecentric/gopass-ui.git\"\n  },\n  \"author\": {\n    \"name\": \"codecentric AG\",\n    \"email\": \"info@codecentric.de\",\n    \"url\": \"https://www.codecentric.de\"\n  },\n  \"contributors\": [\n    {\n      \"name\": \"Matthias Rütten\",\n      \"email\": \"matthias.ruetten@codecentric.de\"\n    },\n    {\n      \"name\": \"Jonas Verhoelen\",\n      \"email\": \"jonas.verhoelen@codecentric.de\"\n    }\n  ],\n  \"license\": \"SEE LICENSE\",\n  \"bugs\": {\n    \"url\": \"https://github.com/codecentric/gopass-ui/issues\"\n  },\n  \"homepage\": \"https://github.com/codecentric/gopass-ui\",\n  \"devDependencies\": {\n    \"@types/dateformat\": \"^3.0.1\",\n    \"@types/electron-devtools-installer\": \"2.2.0\",\n    \"@types/electron-settings\": \"3.1.2\",\n    \"@types/history\": \"^4.7.2\",\n    \"@types/jest\": \"^26.0.23\",\n    \"@types/lodash\": \"^4.14.170\",\n    \"@types/node\": \"^14.17.4\",\n    \"@types/react\": \"^16.8.13\",\n    \"@types/react-dom\": \"^16.8.3\",\n    \"@types/react-hot-loader\": \"^4.1.0\",\n    \"@types/react-router\": \"^4.4.5\",\n    \"@types/react-router-dom\": \"^4.2.0\",\n    \"@types/react-test-renderer\": \"^16.0.0\",\n    \"@types/webdriverio\": \"^5.0.0\",\n    \"@types/webpack-env\": \"^1.16.0\",\n    \"awesome-typescript-loader\": \"^5.2.1\",\n    \"copy-webpack-plugin\": \"^5.1.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"css-loader\": \"^2.1.0\",\n    \"dateformat\": \"^4.5.1\",\n    \"electron\": \"13.6.6\",\n    \"electron-builder\": \"^22.14.3\",\n    \"electron-devtools-installer\": \"3.2.0\",\n    \"electron-mock-ipc\": \"0.3.9\",\n    \"file-loader\": \"^3.0.1\",\n    \"html-webpack-plugin\": \"^3.2.0\",\n    \"husky\": \"^1.3.1\",\n    \"image-webpack-loader\": \"^4.6.0\",\n    \"jest\": \"^27.0.5\",\n    \"node-sass\": \"^7.0.0\",\n    \"pagination-calculator\": \"^1.0.0\",\n    \"prettier\": \"^2.0.5\",\n    \"react-hot-loader\": \"^4.6.3\",\n    \"react-test-renderer\": \"^16.2.0\",\n    \"sass-loader\": \"^7.3.1\",\n    \"source-map-loader\": \"^0.2.4\",\n    \"spectron\": \"^15.0.0\",\n    \"style-loader\": \"^0.23.1\",\n    \"ts-jest\": \"^27.0.3\",\n    \"tslint\": \"^5.15.0\",\n    \"tslint-config-airbnb\": \"^5.4.2\",\n    \"tslint-config-prettier\": \"^1.18.0\",\n    \"tslint-react\": \"^4.0.0\",\n    \"typescript\": \"^3.8.3\",\n    \"webpack\": \"^4.29.0\",\n    \"webpack-cli\": \"^3.3.11\",\n    \"webpack-dev-server\": \"^3.11.0\",\n    \"webpack-merge\": \"^4.2.1\"\n  },\n  \"dependencies\": {\n    \"@electron/remote\": \"^1.2.0\",\n    \"animate.css\": \"^3.7.2\",\n    \"electron-is-accelerator\": \"^0.2.0\",\n    \"electron-log\": \"^4.3.5\",\n    \"electron-settings\": \"^3.2.0\",\n    \"fix-path\": \"^3.0.0\",\n    \"history\": \"^4.10.1\",\n    \"lodash\": \"^4.17.21\",\n    \"material-design-icons\": \"^3.0.1\",\n    \"materialize-css\": \"^1.0.0\",\n    \"promise-timeout\": \"^1.3.0\",\n    \"react\": \"^16.13.1\",\n    \"react-animated-css\": \"^1.2.1\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-keyboard-event-handler\": \"^1.5.4\",\n    \"react-materialize\": \"^2.6.0\",\n    \"react-router\": \"^5.1.2\",\n    \"react-router-dom\": \"^5.1.2\",\n    \"react-treebeard\": \"^3.2.4\",\n    \"string-replace-to-array\": \"^1.0.3\"\n  }\n}\n"
  },
  {
    "path": "src/main/AppUtilities.ts",
    "content": "import * as electronSettings from 'electron-settings'\nimport { DEFAULT_SYSTEM_SETTINGS, DEFAULT_USER_SETTINGS, SystemSettings, UserSettings } from '../shared/settings'\n\nexport const installExtensions = async () => {\n    const installer = require('electron-devtools-installer')\n    const forceDownload = !!process.env.UPGRADE_EXTENSIONS\n    const extensions = ['REACT_DEVELOPER_TOOLS']\n\n    return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.info)\n}\n\nexport const getSystemSettings = (): SystemSettings => {\n    return (electronSettings.get('system_settings') as any) || DEFAULT_SYSTEM_SETTINGS\n}\n\nexport const getUserSettings = (): UserSettings => {\n    return (electronSettings.get('user_settings') as any) || DEFAULT_USER_SETTINGS\n}\n"
  },
  {
    "path": "src/main/AppWindows.ts",
    "content": "import { BrowserWindow, Menu, app, nativeTheme } from 'electron'\nimport * as url from 'url'\nimport * as path from 'path'\n\nexport const createMainWindow = (): BrowserWindow => {\n    nativeTheme.themeSource = 'light'\n    const mainWindow = new BrowserWindow({\n        width: 1000,\n        height: 600,\n        center: true,\n        title: 'Gopass UI',\n        icon: path.join(__dirname, 'assets', 'icon.png'),\n        webPreferences: {\n            enableRemoteModule: true,\n            nodeIntegration: true,\n            contextIsolation: false,\n            worldSafeExecuteJavaScript: false\n        }\n    })\n\n    if (process.env.NODE_ENV !== 'production') {\n        mainWindow.loadURL('http://localhost:2003')\n\n        mainWindow.webContents.openDevTools()\n    } else {\n        mainWindow.loadURL(\n            url.format({\n                pathname: path.join(__dirname, 'explorer', 'index.html'),\n                protocol: 'file:',\n                slashes: true\n            })\n        )\n    }\n\n    return mainWindow\n}\n\nexport const createSearchWindow = (show: boolean): BrowserWindow => {\n    const searchWindow = new BrowserWindow({\n        show,\n        width: process.env.NODE_ENV !== 'production' ? 1200 : 600,\n        height: 600,\n        frame: false,\n        center: true,\n        skipTaskbar: true,\n        title: 'Gopass UI Search Window',\n        resizable: false,\n        webPreferences: {\n            enableRemoteModule: true,\n            nodeIntegration: true,\n            contextIsolation: false,\n            worldSafeExecuteJavaScript: false\n        }\n    })\n\n    searchWindow.setMenu(null)\n\n    if (process.env.NODE_ENV !== 'production') {\n        searchWindow.loadURL('http://localhost:2004')\n\n        searchWindow.webContents.openDevTools()\n    } else {\n        searchWindow.loadURL(\n            url.format({\n                pathname: path.join(__dirname, 'search', 'index.html'),\n                protocol: 'file:',\n                slashes: true\n            })\n        )\n    }\n\n    return searchWindow\n}\n\nexport const hideMainWindow = (mainWindow: BrowserWindow | null) => {\n    if (mainWindow) {\n        if (app.hide) {\n            // Linux and MacOS\n            app.hide()\n        } else {\n            // for Windows\n            mainWindow.blur()\n            mainWindow.hide()\n        }\n    }\n}\n\nexport const buildContextMenu = (mainWindow: BrowserWindow | null, searchWindow: BrowserWindow | null) =>\n    Menu.buildFromTemplate([\n        {\n            label: 'Explorer',\n            click: () => {\n                if (mainWindow) {\n                    mainWindow.show()\n                } else {\n                    mainWindow = createMainWindow()\n                }\n            }\n        },\n        {\n            label: 'Search',\n            click: () => {\n                if (searchWindow) {\n                    searchWindow.show()\n                } else {\n                    searchWindow = createSearchWindow(true)\n                }\n            }\n        },\n        {\n            type: 'separator'\n        },\n        {\n            label: 'Quit',\n            click: () => {\n                app.quit()\n            }\n        }\n    ])\n"
  },
  {
    "path": "src/main/GopassExecutor.ts",
    "content": "import { exec } from 'child_process'\nimport { IpcMainEvent } from 'electron'\n\nexport interface GopassOptions {\n    executionId: string\n    command: string\n    pipeTextInto?: string\n    args?: string[]\n}\n\nexport default class GopassExecutor {\n    public static async handleEvent(event: IpcMainEvent, options: GopassOptions) {\n        const argsString = options.args ? ` ${options.args.join(' ')}` : ''\n        const pipeText = options.pipeTextInto ? `echo \"${options.pipeTextInto}\" | ` : ''\n        const command = `${pipeText}gopass ${options.command}${argsString}`\n\n        exec(command, (err: Error | null, stdout: string, stderr: string) => {\n            event.sender.send(`gopass-answer-${options.executionId}`, {\n                status: err ? 'ERROR' : 'OK',\n                executionId: options.executionId,\n                payload: err ? stderr : stdout\n            })\n        })\n    }\n}\n"
  },
  {
    "path": "src/main/index.ts",
    "content": "import { app, BrowserWindow, Event, globalShortcut, ipcMain, IpcMainEvent, Tray, session, shell, Accelerator } from 'electron'\nimport { URL } from 'url'\nimport * as path from 'path'\nimport * as fixPath from 'fix-path'\nimport * as electronSettings from 'electron-settings'\n\nimport { SystemSettings, UserSettings } from '../shared/settings'\nimport GopassExecutor from './GopassExecutor'\nimport { buildContextMenu, createMainWindow, createSearchWindow } from './AppWindows'\nimport { getSystemSettings, getUserSettings, installExtensions } from './AppUtilities'\n\nconst isDevMode = process.env.NODE_ENV !== 'production'\nfixPath()\n\nimport * as remoteMain from '@electron/remote/main'\nremoteMain.initialize()\n\nlet mainWindow: BrowserWindow | null\nlet searchWindow: BrowserWindow | null\nlet tray: Tray\n\nconst setGlobalSearchWindowShortcut = (shortcut: Accelerator, previousShortcut?: Accelerator) => {\n    // unregister previously used shortcut if Electron recognises it as valid\n    if (previousShortcut) {\n        try {\n            globalShortcut.unregister(previousShortcut)\n        } catch (e) {\n            // previous shortcut was not considered as valid\n        }\n    }\n\n    // unregister shortcut from other usages within the application\n    // in case an error is thrown, Electron does not recognise it as valid and the method returns\n    try {\n        globalShortcut.unregister(shortcut)\n    } catch (e) {\n        return\n    }\n\n    // register shortcut once sure it is a valid and usage-free Accelerator for Electron\n    globalShortcut.register(shortcut, () => {\n        if (searchWindow) {\n            if (searchWindow.isFocused()) {\n                searchWindow.hide()\n            } else {\n                searchWindow.show()\n            }\n        } else {\n            searchWindow = createSearchWindow(true)\n        }\n    })\n}\n\nconst setTray = (showTray: boolean) => {\n    if (showTray) {\n        if (!tray || tray.isDestroyed()) {\n            if (process.platform === 'darwin') {\n                tray = new Tray(path.join(__dirname, 'assets', 'icon-mac@2x.png'))\n            } else if (process.platform === 'linux') {\n                tray = new Tray(path.join(__dirname, 'assets', 'icon@2x.png'))\n            } else {\n                tray = new Tray(path.join(__dirname, 'assets', 'icon.png'))\n            }\n\n            tray.setToolTip('Gopass UI')\n            tray.setContextMenu(buildContextMenu(mainWindow, searchWindow))\n        }\n    } else {\n        if (tray && !tray.isDestroyed()) {\n            tray.destroy()\n        }\n    }\n}\n\nconst listenToIpcEvents = () => {\n    ipcMain.on('gopass', GopassExecutor.handleEvent)\n\n    ipcMain.on('getUserSettings', (event: IpcMainEvent) => {\n        event.returnValue = getUserSettings()\n    })\n\n    ipcMain.on('hideSearchWindow', () => {\n        if (searchWindow) {\n            searchWindow.hide()\n        }\n    })\n\n    ipcMain.on('getSystemSettings', (event: IpcMainEvent) => {\n        event.returnValue = getSystemSettings()\n    })\n\n    ipcMain.on('updateUserSettings', (_: Event, update: Partial<UserSettings>) => {\n        const current = getUserSettings()\n        const all = { ...current, ...update }\n\n        // modify aspects of application where updated settings need take effect\n        if (update.searchShortcut && update.searchShortcut !== current.searchShortcut) {\n            setGlobalSearchWindowShortcut(update.searchShortcut, current.searchShortcut)\n        }\n        if (update.showTray && update.showTray !== current.showTray) {\n            setTray(update.showTray)\n        }\n        if (update.startOnLogin && update.startOnLogin !== current.startOnLogin) {\n            configureStartOnLogin(update.startOnLogin)\n        }\n\n        electronSettings.set('user_settings', all as any)\n    })\n\n    ipcMain.on('updateSystemSettings', (_: Event, update: Partial<SystemSettings>) => {\n        electronSettings.set('system_settings', { ...getSystemSettings(), ...update } as any)\n    })\n}\n\nconst configureStartOnLogin = (startOnLogin: boolean) => {\n    app.setLoginItemSettings({\n        openAtLogin: startOnLogin,\n        openAsHidden: true\n    })\n}\n\nconst setup = async () => {\n    /**\n     * Adds a restrictive default CSP for all fetch directives to all HTTP responses of the web server.\n     * About default-src: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src\n     * Electron reference: https://electronjs.org/docs/tutorial/security#6-define-a-content-security-policy\n     */\n    if (session.defaultSession && !isDevMode) {\n        session.defaultSession.webRequest.onHeadersReceived((details, callback) => {\n            callback({\n                responseHeaders: {\n                    ...details.responseHeaders,\n                    'Content-Security-Policy': [`default-src 'self' 'unsafe-inline'; connect-src https://api.github.com`]\n                }\n            })\n        })\n    }\n\n    if (isDevMode) {\n        await installExtensions()\n    }\n\n    const settings = getUserSettings()\n\n    mainWindow = createMainWindow()\n\n    mainWindow.on('closed', () => {\n        app.quit()\n    })\n\n    searchWindow = createSearchWindow(false)\n\n    setGlobalSearchWindowShortcut(settings.searchShortcut, undefined)\n    setTray(settings.showTray)\n    configureStartOnLogin(settings.startOnLogin)\n\n    listenToIpcEvents()\n}\n\napp.on('ready', setup)\n\napp.on('window-all-closed', () => {\n    if (process.platform !== 'darwin') {\n        app.quit()\n    }\n})\n\napp.on('activate', () => {\n    if (mainWindow === null) {\n        mainWindow = createMainWindow()\n    }\n    if (searchWindow === null) {\n        searchWindow = createSearchWindow(false)\n    }\n})\n\n/**\n * Prevent navigation to every target that lays outside of the Electron application (localhost:2003 and localhost:2004)\n * Reference: https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation\n */\napp.on('web-contents-created', (event, contents) => {\n    contents.on('will-navigate', (navigationEvent, navigationUrl) => {\n        const parsedUrl = new URL(navigationUrl)\n\n        if (!parsedUrl.origin.startsWith('http://localhost:200')) {\n            navigationEvent.preventDefault()\n        }\n    })\n})\n\n/**\n * Prevents unwanted modules from 'remote' from being used.\n * Reference: https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module\n */\nconst allowedRemoteModules = new Set(['app'])\napp.on('remote-get-builtin', (event, webContents, moduleName) => {\n    if (!allowedRemoteModules.has(moduleName)) {\n        event.preventDefault()\n        console.warn(`Blocked module \"${moduleName}\"`)\n    }\n})\n\nconst allowedModules = new Set()\nconst proxiedModules = new Map()\napp.on('remote-require', (event, webContents, moduleName) => {\n    if (proxiedModules.has(moduleName)) {\n        const proxiedModule = proxiedModules.get(moduleName)\n        event.returnValue = proxiedModule\n        console.warn(`Proxied remote-require of module \"${moduleName}\" to \"${proxiedModule}\"`)\n    }\n    if (!allowedModules.has(moduleName)) {\n        event.preventDefault()\n        console.warn(`Blocked remote-require of module \"${moduleName}\"`)\n    }\n})\n\nconst allowedGlobals = new Set()\napp.on('remote-get-global', (event, webContents, globalName) => {\n    if (!allowedGlobals.has(globalName)) {\n        event.preventDefault()\n    }\n})\n\napp.on('remote-get-current-window', event => {\n    event.preventDefault()\n})\n\napp.on('remote-get-current-web-contents', event => {\n    event.preventDefault()\n})\n"
  },
  {
    "path": "src/renderer/common/Settings.ts",
    "content": "import { ipcRenderer } from 'electron'\nimport { SystemSettings, UserSettings } from '../../shared/settings'\nimport set = Reflect.set\n\nexport class Settings {\n    public static getUserSettings(): UserSettings {\n        return ipcRenderer.sendSync('getUserSettings')\n    }\n\n    public static updateUserSettings(settings: Partial<UserSettings>) {\n        ipcRenderer.send('updateUserSettings', settings)\n    }\n\n    public static getSystemSettings(): SystemSettings {\n        return ipcRenderer.sendSync('getSystemSettings')\n    }\n\n    public static updateSystemSettings(settings: Partial<SystemSettings>) {\n        ipcRenderer.send('updateSystemSettings', settings)\n    }\n}\n"
  },
  {
    "path": "src/renderer/common/notifications/Notification.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { Animated } from 'react-animated-css'\n\nimport { useNotificationContext } from './NotificationProvider'\n\nconst DEFAULT_TIMEOUT = 3000\n\nexport default function NotificationView(props: { dismissTimeout?: number }) {\n    const { notification, hide, isHidden } = useNotificationContext()\n    const { dismissTimeout = DEFAULT_TIMEOUT } = props\n\n    React.useEffect(() => {\n        let timeoutId = 0\n\n        if (notification && dismissTimeout !== 0) {\n            timeoutId = window.setTimeout(() => hide(), dismissTimeout)\n        }\n\n        return () => {\n            if (timeoutId !== 0) {\n                clearTimeout(timeoutId)\n            }\n        }\n    }, [notification, dismissTimeout])\n\n    return (\n        <Animated animationIn='fadeInDown' animationInDuration={1000} animationOutDuration={1000} animationOut='fadeOutUp' isVisible={!isHidden}>\n            <m.Row>\n                <m.Col s={12}>\n                    {notification && (\n                        <m.CardPanel className={`${notification.status === 'OK' ? 'green' : 'red'} lighten-1 black-text`}>\n                            <m.Row style={{ marginBottom: 0 }}>\n                                <m.Col s={11}>\n                                    <span>{notification ? notification.message : ''}</span>\n                                </m.Col>\n                                <m.Col s={1} style={{ textAlign: 'right' }}>\n                                    <a className='black-text link' onClick={() => hide()}>\n                                        <m.Icon small>close</m.Icon>\n                                    </a>\n                                </m.Col>\n                            </m.Row>\n                        </m.CardPanel>\n                    )}\n                </m.Col>\n            </m.Row>\n        </Animated>\n    )\n}\n"
  },
  {
    "path": "src/renderer/common/notifications/NotificationProvider.tsx",
    "content": "import * as React from 'react'\n\nexport interface Notification {\n    status: 'OK' | 'ERROR'\n    message: string\n}\n\nexport interface NotificationContext {\n    notification?: Notification\n    isHidden: boolean\n    show: (notification: Notification) => void\n    hide: () => void\n}\n\nconst Context = React.createContext<NotificationContext | null>(null)\n\nexport function useNotificationContext() {\n    const context = React.useContext(Context)\n\n    if (!context) {\n        throw Error('NO Context!')\n    }\n\n    return context\n}\n\nexport default function NotificationProvider({ children }: any) {\n    const [notification, setNotification] = React.useState<Notification | undefined>()\n    const [isHidden, setIsHidden] = React.useState<boolean>(false)\n\n    return (\n        <Context.Provider\n            value={{\n                notification,\n                isHidden,\n                show: newNotification => {\n                    setNotification(newNotification)\n                    setIsHidden(false)\n                },\n                hide: () => {\n                    setIsHidden(true)\n                    setTimeout(() => {\n                        setNotification(undefined)\n                    }, 1000)\n                }\n            }}\n        >\n            {children}\n        </Context.Provider>\n    )\n}\n"
  },
  {
    "path": "src/renderer/components/ExternalLink.tsx",
    "content": "import * as React from 'react'\nimport { shell } from 'electron'\n\nexport function ExternalLink(props: { url: string; children: any }) {\n    const { url, children } = props\n\n    return (\n        <a className='link' onClick={() => shell.openExternal(url)}>\n            {children}\n        </a>\n    )\n}\n"
  },
  {
    "path": "src/renderer/components/GoBackNavigationButton.tsx",
    "content": "import * as React from 'react'\nimport { withRouter } from 'react-router'\nimport { History } from 'history'\n\nimport { RoundActionButton } from './RoundActionButton'\n\nconst GoBackNavigationButton = (props: { history: History }) => (\n    <div style={{ paddingTop: '0.75rem' }}>\n        <RoundActionButton icon='arrow_back' onClick={() => props.history.replace('/')} />\n    </div>\n)\n\nexport default withRouter(GoBackNavigationButton)\n"
  },
  {
    "path": "src/renderer/components/PaginatedTable.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { paginationCalculator } from 'pagination-calculator'\nimport { PageInformation } from 'pagination-calculator/dist/paginationCalculator'\n\nexport interface TableColumn {\n    fieldName: string\n    label: string\n}\n\nexport interface TableRow {\n    id: string\n\n    [fieldName: string]: string | React.ReactNode\n}\n\nexport interface PaginatedTableProps {\n    columns: TableColumn[]\n    rows: TableRow[]\n}\n\nexport interface PaginatedTableState {\n    page: number\n    pageSize: number\n}\n\nexport default class PaginatedTable extends React.Component<PaginatedTableProps, PaginatedTableState> {\n    constructor(props: PaginatedTableProps) {\n        super(props)\n        this.state = {\n            page: 1,\n            pageSize: 7 // TODO: make configurable through settings page\n        }\n    }\n\n    public render() {\n        const { columns, rows } = this.props\n        const pagination = paginationCalculator({\n            total: rows.length,\n            current: this.state.page,\n            pageSize: this.state.pageSize,\n            pageLimit: Math.ceil(rows.length / this.state.pageSize)\n        })\n        const pageRows = rows.slice(pagination.showingStart - 1, pagination.showingEnd)\n\n        return (\n            <>\n                <m.Table>\n                    <thead>\n                        <tr>\n                            {columns.map(column => (\n                                <th key={column.fieldName} data-field={column.fieldName}>\n                                    {column.label}\n                                </th>\n                            ))}\n                        </tr>\n                    </thead>\n\n                    <tbody>\n                        {pageRows.map(row => (\n                            <tr key={row.id}>\n                                {columns.map(column => (\n                                    <td key={`${row.id}-${column.fieldName}`}>{row[column.fieldName]}</td>\n                                ))}\n                            </tr>\n                        ))}\n                    </tbody>\n                </m.Table>\n\n                {this.renderPagination(pagination)}\n            </>\n        )\n    }\n\n    private renderPagination(pagination: PageInformation) {\n        return (\n            <m.Pagination\n                items={pagination.pageCount}\n                activePage={pagination.current}\n                maxButtons={pagination.pageCount <= 8 ? pagination.pageCount : 8}\n                onSelect={this.changeToPage(pagination.pageCount)}\n            />\n        )\n    }\n\n    private changeToPage = (maxPageNumber: number) => {\n        return (page: number) => {\n            if (page <= maxPageNumber && page >= 1) {\n                this.setState({ page })\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/renderer/components/RoundActionButton.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nexport interface RoundActionBtnProps {\n    icon: string\n    onClick?: () => void\n}\n\nexport const RoundActionButton = ({ icon, onClick }: RoundActionBtnProps) => (\n    <m.Button floating large className='red' waves='light' icon={icon} onClick={onClick} style={{ marginRight: '0.75rem' }} />\n)\n"
  },
  {
    "path": "src/renderer/components/loading-screen/LoadingScreen.css",
    "content": ".loading-screen-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 500px;\n    font-size: 24px;\n}\n\n.loading-screen-message {\n    width: 40%;\n    min-width: 300px;\n    text-align: center;\n}\n"
  },
  {
    "path": "src/renderer/components/loading-screen/LoadingScreen.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport './LoadingScreen.css'\n\nconst WAITING_TEXTS = [\n    'Loading, please wait...',\n    'Still doing something...',\n    'It seems to takes some time...',\n    'Doop di doop di douuu...',\n    \"I'm sorry that it takes longer.\",\n    'Maybe there is a persistent problem. Sorry for that!'\n]\n\nexport function LoadingScreen() {\n    const [waitingTextIndex, setWaitingTextIndex] = React.useState(0)\n\n    React.useEffect(() => {\n        const timeout = setInterval(() => {\n            setWaitingTextIndex(waitingTextIndex + 1)\n        }, 2000)\n\n        return () => clearInterval(timeout)\n    }, [])\n\n    return (\n        <div className='loading-screen-wrapper'>\n            <div className='loading-screen-message'>\n                <p>{WAITING_TEXTS[waitingTextIndex % WAITING_TEXTS.length]}</p>\n                <m.ProgressBar />\n            </div>\n        </div>\n    )\n}\n"
  },
  {
    "path": "src/renderer/components/tree/TreeComponent.tsx",
    "content": "import * as React from 'react'\nimport * as t from 'react-treebeard'\nimport { globalStyle } from './TreeStyle'\nimport { TreeHeader as Header } from './TreeHeader'\n\nexport interface Tree {\n    name: string\n    toggled?: boolean\n    loading?: boolean\n    children?: Tree[]\n    path: string\n}\n\nexport interface TreeComponentProps {\n    tree: Tree\n    onLeafClick: (leafId: string) => void\n}\n\ninterface TreeComponentState {\n    selectedNode?: any\n}\nexport default class TreeComponent extends React.Component<TreeComponentProps, TreeComponentState> {\n    public state: TreeComponentState = {}\n\n    public render() {\n        return <t.Treebeard data={this.props.tree} decorators={{ ...t.decorators, Header }} onToggle={this.onToggle} style={globalStyle} />\n    }\n\n    private onToggle = (node: any, toggled: boolean) => {\n        // if no children (thus being a leaf and thereby an entry), trigger the handler\n        if (!node.children || node.children.length === 0) {\n            this.props.onLeafClick(node.path)\n        }\n\n        // previously selected node is no more active\n        if (this.state.selectedNode) {\n            this.state.selectedNode.active = false\n        }\n\n        // newly selected node shall be active\n        node.active = true\n\n        // ...and toggled if having children\n        if (node.children) {\n            node.toggled = toggled\n        }\n        this.setState({ selectedNode: node })\n\n        if (node.children && node.children.length === 1) {\n            this.onToggle(node.children[0], true)\n        }\n    }\n}\n"
  },
  {
    "path": "src/renderer/components/tree/TreeHeader.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport { deriveIconFromSecretName } from '../../secrets/deriveIconFromSecretName'\n\nexport const TreeHeader = ({ style, node }: any) => {\n    let iconType = 'folder'\n\n    const isLeaf = !node.children && node.path\n    if (!node.children && node.path) {\n        iconType = deriveIconFromSecretName(node.name)\n    }\n    return (\n        <div style={style.base} className='icon-wrapper'>\n            {node.children && (\n                <div className={`chevron ${node.toggled && 'toggled'}`}>\n                    <m.Icon small>chevron_right</m.Icon>\n                </div>\n            )}\n            <m.Icon small>{iconType}</m.Icon>\n\n            {node.name}\n        </div>\n    )\n}\n"
  },
  {
    "path": "src/renderer/components/tree/TreeStyle.ts",
    "content": "const white = '#FFFFFF'\nconst none = 'none'\n\nexport const globalStyle = {\n    tree: {\n        base: {\n            listStyle: none,\n            backgroundColor: white,\n            margin: 0,\n            padding: 0,\n            color: '#000000',\n            fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif;',\n            fontSize: '18px'\n        },\n        node: {\n            base: {\n                position: 'relative',\n                borderLeft: '1px solid #ececec',\n                padding: '4px 12px 4px',\n                marginLeft: '8px'\n            },\n            link: {\n                cursor: 'pointer',\n                position: 'relative',\n                display: 'block'\n            },\n            activeLink: {\n                background: white\n            },\n            toggle: {\n                base: {\n                    display: none\n                },\n                wrapper: {\n                    position: 'absolute',\n                    top: '50%',\n                    left: '50%',\n                    margin: '-7px 0 0 -7px',\n                    height: '14px'\n                },\n                height: 0,\n                width: 0,\n                arrow: {\n                    fill: white,\n                    strokeWidth: 0\n                }\n            },\n            header: {\n                base: {\n                    display: 'inline-block',\n                    verticalAlign: 'top',\n                    color: '#555',\n                    cursor: 'pointer'\n                }\n            },\n            subtree: {\n                listStyle: none,\n                paddingLeft: '60px'\n            },\n            loading: {\n                color: '#E2C089'\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/ExplorerApplication.css",
    "content": ".secret-explorer {\n    position: fixed;\n    z-index: 10;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    width: 450px;\n    overflow: auto;\n    min-width: 300px;\n    border-right: 1px solid rgba(0, 0, 0, 0.14);\n}\n\n.secret-explorer .search-bar {\n    background: #f9f9f9;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.14);\n    position: sticky;\n    top: 0;\n    z-index: 10;\n}\n\n.secret-explorer .search-bar * {\n    margin: 0 !important;\n    border: none !important;\n}\n\n.secret-explorer .search-bar > div {\n    padding: 0 !important;\n}\n\n.secret-explorer .search-bar input {\n    box-sizing: border-box !important;\n    padding: 24px !important;\n}\n\n.secret-explorer > ul > li {\n    border: 0;\n}\n\n.secret-explorer .chevron {\n    display: inline-block;\n    user-select: none;\n}\n\n.secret-explorer .chevron.toggled .material-icons {\n    transform: rotate(90deg);\n}\n\n.secret-explorer .icon-wrapper > .material-icons:only-child {\n    margin-left: 30px;\n}\n\n.secret-explorer .chevron .material-icons {\n    margin: 0;\n}\n\n.secret-explorer i.material-icons {\n    position: relative;\n    top: 7px;\n    margin-right: 8px;\n}\n\n.main-content {\n    resize: both;\n    padding-left: 450px;\n}\n\n.m-top {\n    margin-top: 55px;\n}\n\n.panel-headline {\n    margin-top: 0;\n}\n\n.link {\n    cursor: pointer;\n}\n\nspan.code {\n    font-family: Consolas, monospace;\n}\n\n.card-panel pre {\n    margin-bottom: 0px;\n}\n\n.password-strength-sum {\n    height: 100px;\n    width: 100px;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n    border-radius: 50%;\n    display: flex;\n    font-size: 42px;\n    font-weight: 200;\n}\n\n.password-strength-sum.red {\n    color: white;\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/ExplorerApplication.tsx",
    "content": "import * as React from 'react'\n\nimport SecretExplorer from './side-navigation/SecretExplorer'\nimport MainContent from './MainContent'\n\nimport './ExplorerApplication.css'\n\nconst ExplorerApplication = () => {\n    return (\n        <>\n            <SecretExplorer />\n            <MainContent />\n        </>\n    )\n}\n\nexport default ExplorerApplication\n"
  },
  {
    "path": "src/renderer/explorer-app/GithubService.ts",
    "content": "export interface GithubTag {\n    url: string\n    ref: string\n}\n\nexport default class GithubService {\n    public static getTagsOfRepository(owner: string, repositoryName: string): Promise<GithubTag[]> {\n        return new Promise((resolve, reject) => {\n            const httpRequest = new XMLHttpRequest()\n            const url = `https://api.github.com/repos/${owner}/${repositoryName}/git/refs/tags`\n\n            httpRequest.open('GET', url)\n            httpRequest.onload = e => {\n                if (httpRequest.status >= 200 && httpRequest.status < 300) {\n                    resolve(JSON.parse(httpRequest.response) as GithubTag[])\n                } else {\n                    reject({\n                        status: httpRequest.status,\n                        statusText: httpRequest.statusText\n                    })\n                }\n            }\n            httpRequest.send()\n        })\n    }\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/MainContent.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { match, Route } from 'react-router-dom'\n\nimport SettingsPage from './pages/SettingsPage'\nimport HomePage from './pages/HomePage'\nimport MainNavigation from './components/MainNavigation'\nimport GoBackNavigation from '../components/GoBackNavigationButton'\nimport Notification from '../common/notifications/Notification'\nimport NotificationProvider from '../common/notifications/NotificationProvider'\nimport PasswordHealthOverview from './pages/PasswordHealthPage'\nimport AddSecretPage from './pages/AddSecretPage'\nimport SecretDetailsPage from './pages/details/SecretDetailsPage'\nimport MountsPage from './pages/MountsPage'\nimport AddMountPage from './pages/AddMountPage'\n\nconst Routes = () => (\n    <>\n        <Route\n            path='/'\n            exact\n            render={() => (\n                <>\n                    <MainNavigation />\n                    <HomePage />\n                </>\n            )}\n        />\n        <Route\n            path='/secret/:encodedSecretName'\n            component={(props: { match: match<{ encodedSecretName: string }>; location: { search?: string } }) => {\n                const secretName = atob(props.match.params.encodedSecretName)\n                const isAdded = props.location.search ? props.location.search === '?added' : false\n\n                return (\n                    <>\n                        <MainNavigation />\n                        <SecretDetailsPage secretName={secretName} isAdded={isAdded} />\n                    </>\n                )\n            }}\n        />\n        <Route\n            path='/settings'\n            exact\n            render={() => (\n                <>\n                    <GoBackNavigation />\n                    <SettingsPage />\n                </>\n            )}\n        />\n        <Route path='/mounts' exact render={() => <MountsPage />} />\n        <Route\n            path='/password-health'\n            exact\n            render={() => (\n                <>\n                    <GoBackNavigation />\n                    <PasswordHealthOverview />\n                </>\n            )}\n        />\n        <Route path='/add-mount' exact render={() => <AddMountPage />} />\n        <Route\n            path='/add-secret'\n            exact\n            render={() => (\n                <>\n                    <GoBackNavigation />\n                    <AddSecretPage />\n                </>\n            )}\n        />\n    </>\n)\n\nconst MainContent = () => (\n    <div className='main-content'>\n        <NotificationProvider>\n            <Notification />\n            <m.Row>\n                <m.Col s={12}>\n                    <Routes />\n                </m.Col>\n            </m.Row>\n        </NotificationProvider>\n    </div>\n)\n\nexport default MainContent\n"
  },
  {
    "path": "src/renderer/explorer-app/SecretsProvider.tsx",
    "content": "import * as React from 'react'\nimport { Tree } from '../components/tree/TreeComponent'\nimport SecretsFilterService from './side-navigation/SecretsFilterService'\nimport SecretsDirectoryService from './side-navigation/SecretsDirectoryService'\nimport Gopass from '../secrets/Gopass'\n\nexport interface SecretsContext {\n    tree: Tree\n    searchValue: string\n\n    applySearchToTree: (searchValue: string) => void\n    reloadSecretNames: () => Promise<void>\n}\n\nconst Context = React.createContext<SecretsContext | undefined>(undefined)\nexport const useSecretsContext = () => {\n    const context = React.useContext(Context)\n    if (!context) {\n        throw Error('no secrets context!')\n    }\n\n    return context\n}\n\nexport const SecretsProvider = ({ children }: { children: React.ReactNode }) => {\n    const [tree, setTree] = React.useState<Tree>({ name: 'Stores', toggled: true, children: [], path: '' })\n    const [searchValue, setSearchValue] = React.useState<string>('')\n    const [secretNames, setSecretNames] = React.useState<string[]>([])\n\n    const applySearchToTree = (newSearchValue?: string, updatedSecretNames?: string[]) => {\n        let searchValueToUse = searchValue\n        if (newSearchValue !== undefined) {\n            setSearchValue(newSearchValue)\n            searchValueToUse = newSearchValue\n        }\n        const filteredSecretNames = SecretsFilterService.filterBySearch(updatedSecretNames || secretNames, searchValueToUse)\n        const newTree: Tree = SecretsDirectoryService.secretPathsToTree(filteredSecretNames, tree, filteredSecretNames.length <= 15)\n\n        setTree(newTree)\n    }\n    const loadSecretsAndBuildTree = async (newSearchValue: string | undefined = undefined) => {\n        const allSecretNames = await Gopass.getAllSecretNames()\n        setSecretNames(allSecretNames)\n        applySearchToTree(newSearchValue !== undefined ? newSearchValue : searchValue, allSecretNames)\n    }\n\n    const providerValue: SecretsContext = {\n        tree,\n        searchValue,\n        reloadSecretNames: () => loadSecretsAndBuildTree(),\n        applySearchToTree\n    }\n\n    return <Context.Provider value={providerValue}>{children}</Context.Provider>\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/components/EnvironmentTest.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { timeout } from 'promise-timeout'\nimport { shell } from 'electron'\n\nimport Gopass from '../../secrets/Gopass'\nimport { ExternalLink } from '../../components/ExternalLink'\nimport { Settings } from '../../common/Settings'\n\ntype ErrorDetails = 'GOPASS_CONNECTION' | 'DECRYPTION' | undefined\n\nexport function EnvironmentTest() {\n    const [environmentTestStatus, setEnvironmentTestStatus] = React.useState<'PENDING' | 'RUNNING' | 'OK' | 'ERROR'>('PENDING')\n    const [errorDetails, setErrorDetails] = React.useState<ErrorDetails>()\n\n    function reset() {\n        setEnvironmentTestStatus('PENDING')\n        setErrorDetails(undefined)\n    }\n\n    function executeTest() {\n        setEnvironmentTestStatus('RUNNING')\n\n        timeout(Gopass.getAllSecretNames(), 1500)\n            .then(([firstEntry]) => {\n                if (firstEntry) {\n                    timeout(Gopass.show(firstEntry), 10000)\n                        .then(() => {\n                            setEnvironmentTestStatus('OK')\n                            Settings.updateSystemSettings({ environmentTestSuccessful: true })\n                        })\n                        .catch(() => {\n                            setEnvironmentTestStatus('ERROR')\n                            Settings.updateSystemSettings({ environmentTestSuccessful: false })\n                            setErrorDetails('DECRYPTION')\n                        })\n                }\n            })\n            .catch(() => {\n                setEnvironmentTestStatus('ERROR')\n                Settings.updateSystemSettings({ environmentTestSuccessful: false })\n                setErrorDetails('GOPASS_CONNECTION')\n            })\n    }\n\n    switch (environmentTestStatus) {\n        case 'PENDING':\n            return <PendingContent executeTest={executeTest} />\n        case 'RUNNING':\n            return <RunningContent />\n        case 'OK':\n            return <OkContent />\n        default:\n        case 'ERROR':\n            return <ErrorContent errorDetails={errorDetails} reset={reset} />\n    }\n}\n\nfunction PendingContent(props: { executeTest: () => void }) {\n    return (\n        <>\n            Your system has to meet the following requirements for Gopass UI to work properly:\n            <ol>\n                <li>Gopass needs to be installed and configured to be up and running 🙂</li>\n                <li>\n                    MacOS: you should use <span className='code'>pinentry-mac</span> as a GPG passphrase dialog tool (available{' '}\n                    <ExternalLink url='https://formulae.brew.sh/formula/pinentry-mac'>as Brew formulae</ExternalLink>)\n                </li>\n            </ol>\n            <p>During the environment test you might be asked for your GPG passphrase. Please unlock your GPG keypair by entering it.</p>\n            <m.Button onClick={props.executeTest} waves='light'>\n                Test your environment\n            </m.Button>\n        </>\n    )\n}\n\nfunction RunningContent() {\n    return (\n        <div style={{ textAlign: 'center' }}>\n            <m.Preloader size='small' />\n            <br />\n            <br />\n            <strong>Tests are running...</strong>\n        </div>\n    )\n}\n\nfunction ErrorContent(props: { errorDetails: ErrorDetails; reset: () => void }) {\n    return (\n        <div style={{ textAlign: 'center' }}>\n            <m.Icon large>error</m.Icon>\n            <br />\n            <h4>Oops, something went wrong.</h4>\n            {props.errorDetails && (\n                <>\n                    <strong>\n                        {props.errorDetails === 'DECRYPTION' && <>It wasn't possible to decrypt your secrets.</>}\n                        {props.errorDetails === 'GOPASS_CONNECTION' && <>It wasn't possible to access the gopass CLI.</>}\n                    </strong>\n                    <br />\n                    <br />\n                </>\n            )}\n            Do you need help getting started?{' '}\n            <a onClick={() => shell.openExternal('https://github.com/codecentric/gopass-ui/issues')}>Please create an issue.</a>\n            <br />\n            <br />\n            <m.Button onClick={props.reset} waves='light'>\n                restart\n            </m.Button>\n        </div>\n    )\n}\n\nfunction OkContent() {\n    return (\n        <div style={{ textAlign: 'center' }}>\n            <m.Icon large>done</m.Icon>\n            <br />\n            <strong>Everything looks fine</strong>\n        </div>\n    )\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/components/LastVersionInfo.tsx",
    "content": "import * as React from 'react'\nimport { app } from '@electron/remote'\nimport GithubService, { GithubTag } from '../GithubService'\nimport { ExternalLink } from '../../components/ExternalLink'\nimport { Settings } from '../../common/Settings'\n\nconst ONE_HOUR_IN_MILLIS = 3600000\nconst VERSION_CHECK_INTERVAL = ONE_HOUR_IN_MILLIS\n\nexport const LatestVersionInfo = () => {\n    const { releaseCheck } = Settings.getSystemSettings()\n    const [tags, setTags] = React.useState<GithubTag[]>([])\n\n    React.useEffect(() => {\n        const millisNow = new Date().getTime()\n        const shouldFetchTags = !releaseCheck || !releaseCheck.lastCheckTimestamp || millisNow - VERSION_CHECK_INTERVAL > releaseCheck.lastCheckTimestamp\n\n        if (shouldFetchTags) {\n            GithubService.getTagsOfRepository('codecentric', 'gopass-ui').then(newTags => {\n                setTags(newTags)\n                Settings.updateSystemSettings({ releaseCheck: { lastCheckTimestamp: millisNow, results: newTags } })\n            })\n        } else {\n            setTags(releaseCheck.results)\n        }\n    }, [])\n\n    const lastTag = tags[tags.length - 1]\n    const lastTagName = lastTag ? lastTag.ref.slice(10, lastTag.ref.length) : ''\n    const appVersion = app.getVersion()\n\n    if (lastTagName) {\n        return lastTagName.includes(appVersion) ? (\n            <>\n                You have the latest version <strong>{appVersion}</strong> of Gopass UI installed 🎉\n            </>\n        ) : (\n            <>\n                Your Gopass UI version ({appVersion}) is out of date 😕 Make sure you got the latest release of Gopass UI:&nbsp;\n                <ExternalLink url='https://github.com/codecentric/gopass-ui/releases/latest'>{`${lastTagName} on Github`}</ExternalLink>\n            </>\n        )\n    }\n\n    return null\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/components/MainNavigation.tsx",
    "content": "import * as React from 'react'\nimport { History } from 'history'\nimport { withRouter } from 'react-router'\n\nimport { RoundActionButton } from '../../components/RoundActionButton'\nimport { useNotificationContext } from '../../common/notifications/NotificationProvider'\nimport Gopass from '../../secrets/Gopass'\nimport { useSecretsContext } from '../SecretsProvider'\n\ninterface MainNavigationViewProps {\n    history: History\n}\n\nfunction MainNavigationComponent({ history }: MainNavigationViewProps) {\n    const notificationContext = useNotificationContext()\n    const secretsContext = useSecretsContext()\n\n    const refreshGopassStores = async () => {\n        try {\n            await Gopass.sync()\n            await secretsContext.reloadSecretNames()\n            notificationContext.show({ status: 'OK', message: 'Your stores have been synchronised successfully.' })\n        } catch (err) {\n            notificationContext.show({ status: 'ERROR', message: `Oops, something went wrong: ${JSON.stringify(err)}` })\n        }\n    }\n\n    return (\n        <div style={{ paddingTop: '0.75rem' }}>\n            <RoundActionButton icon='home' onClick={() => history.replace('/')} />\n            <RoundActionButton icon='add' onClick={() => history.replace('/add-secret')} />\n            <RoundActionButton icon='refresh' onClick={refreshGopassStores} />\n            <RoundActionButton icon='settings' onClick={() => history.replace('/settings')} />\n            <RoundActionButton icon='storage' onClick={() => history.replace('/mounts')} />\n            <RoundActionButton icon='security' onClick={() => history.replace('/password-health')} />\n        </div>\n    )\n}\n\nexport default withRouter(MainNavigationComponent)\n"
  },
  {
    "path": "src/renderer/explorer-app/components/PasswordStrengthInfo.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\ninterface PasswordStrengthInfoProps {\n    strength: number\n    labelContent: any\n}\n\nexport const PasswordStrengthInfo = ({ strength, labelContent }: PasswordStrengthInfoProps) => (\n    <>\n        <m.Col s={4}>\n            <label className='active'>\n                {labelContent}: {strength + ' %'}\n            </label>\n            <m.ProgressBar progress={strength} />\n        </m.Col>\n    </>\n)\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/AddMountPage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'react-router'\nimport Gopass, { Mount } from '../../secrets/Gopass'\nimport { RoundActionButton } from '../../components/RoundActionButton'\nimport { useSecretsContext } from '../SecretsProvider'\nimport { useNotificationContext } from '../../common/notifications/NotificationProvider'\n\nfunction AddMountPage({ history }: RouteComponentProps) {\n    const notificationContext = useNotificationContext()\n    const secretsContext = useSecretsContext()\n    const [mount, setMount] = React.useState<Mount>({ name: '', path: '' })\n\n    const addMount = async () => {\n        if (mount.path && mount.name) {\n            try {\n                await Gopass.addMount(mount)\n                await secretsContext.reloadSecretNames()\n                history.replace('/mounts')\n            } catch (err) {\n                if (typeof err === 'string') {\n                    if (err === 'duplicate-name') {\n                        notificationContext.show({ status: 'ERROR', message: `A mount named \"${mount.name}\" does already exist` })\n                    }\n\n                    if (err.includes('Doubly mounted path')) {\n                        notificationContext.show({\n                            status: 'ERROR',\n                            message: `The path \"${mount.path}\" is already in use by another mount`\n                        })\n                    }\n                } else {\n                    notificationContext.show({\n                        status: 'ERROR',\n                        message: `Unexpected error while adding mount: ${JSON.stringify(err)}`\n                    })\n                }\n            }\n        }\n    }\n\n    return (\n        <>\n            <div style={{ paddingTop: '0.75rem' }}>\n                <RoundActionButton icon='arrow_back' onClick={() => history.replace('/mounts')} />\n            </div>\n\n            <h4>New Mount</h4>\n\n            <m.CardPanel>Create a new mount that shall be managed by Gopass as a password store from now on.</m.CardPanel>\n            <m.Row>\n                <m.Input s={12} value={mount.name} onChange={(_: any, value: string) => setMount({ ...mount, name: value })} label='Name' />\n                <m.Input s={12} value={mount.path} onChange={(_: any, value: string) => setMount({ ...mount, path: value })} label='Directory path' />\n\n                <m.Col s={12}>\n                    <m.Button disabled={!mount.name || !mount.path} onClick={addMount} waves='light'>\n                        Save\n                    </m.Button>\n                </m.Col>\n            </m.Row>\n        </>\n    )\n}\n\nexport default withRouter(AddMountPage)\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/AddSecretPage.css",
    "content": ".secret-value-textarea {\n    height: 64px;\n    font-size: 16px;\n    resize: vertical;\n    border: 1px solid #9e9e9e;\n    margin-top: 5px;\n}\n\n.secret-value-field-row {\n    margin: 0 0.75rem;\n    width: calc(100% - 1.5rem);\n}\n\n.secret-value-label {\n    fontSize: 0.8rem;\n    color: #9e9e9e;\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/AddSecretPage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'react-router'\nimport Gopass from '../../secrets/Gopass'\nimport { passwordSecretRegex } from '../../secrets/deriveIconFromSecretName'\nimport { PasswordStrengthInfo } from '../components/PasswordStrengthInfo'\nimport { PasswordRater } from '../password-health/PasswordRater'\n\nimport './AddSecretPage.css'\nimport { Settings } from '../../common/Settings'\nimport { useSecretsContext } from '../SecretsProvider'\n\ninterface AddSecretPageState {\n    name?: string\n    value?: string\n}\n\nclass AddSecretPage extends React.Component<RouteComponentProps, AddSecretPageState> {\n    constructor(props: any) {\n        super(props)\n        this.state = {\n            name: undefined,\n            value: this.generateRandomValue(Settings.getUserSettings().secretValueLength)\n        }\n    }\n\n    public render() {\n        const { name, value } = this.state\n        const nameIndicatesPassword = name ? passwordSecretRegex.test(name) : false\n        const entity = nameIndicatesPassword ? 'Password' : 'Secret'\n        const nameLabel = `Secret name (${nameIndicatesPassword ? 'detected password' : 'e.g. store/my/new/secret/name'})`\n        const valueLabel = `${entity} value`\n        const shuffleButtonLabel = `Shuffle ${nameIndicatesPassword ? 'password' : 'value'}`\n        const currentPasswordValueRating = PasswordRater.ratePassword(value || '')\n\n        return (\n            <>\n                <h4>New {entity}</h4>\n\n                <m.CardPanel>\n                    Adds new secrets to your Gopass stores. After clicking the Add-button, your new secret will be pushed to remote directly.\n                </m.CardPanel>\n\n                <m.Row>\n                    <m.Input s={12} value={name} onChange={this.changeName} label={nameLabel} />\n                    <div className='secret-value-field-row'>\n                        <label className='secret-value-label'>{valueLabel}</label>\n                        <textarea className='secret-value-textarea' placeholder={valueLabel} value={value} onChange={this.changeValue} />\n                    </div>\n                    <PasswordStrengthInfo strength={currentPasswordValueRating.health} labelContent={`${entity} value strength`} />\n                    <m.Col s={12}>\n                        <m.Button style={{ marginRight: '10px' }} onClick={this.shuffleRandomValue} waves='light'>\n                            {shuffleButtonLabel}\n                        </m.Button>\n                        <m.Button disabled={!name || !value} onClick={this.addSecret} waves='light'>\n                            Save\n                        </m.Button>\n                    </m.Col>\n                </m.Row>\n            </>\n        )\n    }\n\n    private changeName = (_: any, name: string) => this.setState({ name })\n    private changeValue = (event: any) => this.setState({ value: event.target.value })\n\n    private generateRandomValue = (length: number) => {\n        const chars = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()-+<>ABCDEFGHIJKLMNOP1234567890'\n        let randomPassword = ''\n\n        for (let x = 0; x < length; x++) {\n            const randomIndex = Math.floor(Math.random() * chars.length)\n            randomPassword += chars.charAt(randomIndex)\n        }\n\n        return randomPassword\n    }\n\n    private shuffleRandomValue = () => this.setState({ value: this.generateRandomValue(Settings.getUserSettings().secretValueLength) })\n\n    private addSecret = async () => {\n        const { name, value } = this.state\n\n        if (name && value) {\n            const { history } = this.props\n\n            try {\n                await Gopass.addSecret(name, value)\n                await useSecretsContext().reloadSecretNames()\n                history.replace(`/secret/${btoa(name)}?added`)\n            } catch (e) {\n                console.info('Error during adding a secret', e)\n            }\n        }\n    }\n}\n\nexport default withRouter(AddSecretPage)\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/HomePage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport { EnvironmentTest } from '../components/EnvironmentTest'\nimport { LatestVersionInfo } from '../components/LastVersionInfo'\nimport { ExternalLink } from '../../components/ExternalLink'\nimport { Settings } from '../../common/Settings'\n\nconst OptionalSetupInstructions = () => {\n    const { environmentTestSuccessful } = Settings.getSystemSettings()\n\n    return environmentTestSuccessful ? null : (\n        <>\n            <h4>Environment Test</h4>\n            <m.CardPanel>\n                <EnvironmentTest />\n            </m.CardPanel>\n        </>\n    )\n}\n\nconst HomePage = () => {\n    const { searchShortcut } = Settings.getUserSettings()\n\n    return (\n        <>\n            <h3>Welcome to Gopass UI</h3>\n            <OptionalSetupInstructions />\n\n            <m.CardPanel>\n                Choose a secret from the navigation or use the actions at the top. <LatestVersionInfo />\n            </m.CardPanel>\n\n            <h4 className='m-top'>Global search window</h4>\n            <m.CardPanel>\n                The configured shortcut for the global search window (quick secret clipboard-copying) is:\n                <pre>{searchShortcut.replace(/\\+/g, ' + ')}</pre>\n            </m.CardPanel>\n\n            <h4 className='m-top'>Issues</h4>\n            <m.CardPanel>\n                Please report any issues and problems to us on <ExternalLink url='https://github.com/codecentric/gopass-ui/issues'>Github</ExternalLink>.<br />\n                We are very keen about your feedback and appreciate any help.\n            </m.CardPanel>\n        </>\n    )\n}\n\nexport default HomePage\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/MountsPage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'react-router'\n\nimport Gopass, { Mount } from '../../secrets/Gopass'\nimport PaginatedTable from '../../components/PaginatedTable'\nimport { LoadingScreen } from '../../components/loading-screen/LoadingScreen'\nimport { RoundActionButton } from '../../components/RoundActionButton'\nimport { useSecretsContext } from '../SecretsProvider'\n\nconst MountsPage = ({ history }: RouteComponentProps) => {\n    const secretsContext = useSecretsContext()\n    const [mounts, setMounts] = React.useState<Mount[] | undefined>(undefined)\n    const [loading, setLoading] = React.useState(true)\n\n    const loadMounts = async () => {\n        setLoading(true)\n        const allMounts = await Gopass.getAllMounts()\n        setMounts(allMounts)\n        setLoading(false)\n    }\n    const deleteMount = async (name: string) => {\n        await Gopass.deleteMount(name)\n        await secretsContext.reloadSecretNames()\n        await loadMounts()\n    }\n\n    React.useEffect(() => {\n        loadMounts()\n    }, [])\n\n    return (\n        <>\n            <div style={{ paddingTop: '0.75rem' }}>\n                <RoundActionButton icon='arrow_back' onClick={() => history.replace('/')} />\n                <RoundActionButton icon='add' onClick={() => history.replace('/add-mount')} />\n            </div>\n\n            <h4>Mounts</h4>\n            {loading && <LoadingScreen />}\n            {!loading && mounts && mounts.length === 0 && <m.CardPanel>No mounts available.</m.CardPanel>}\n            {!loading && mounts && mounts.length > 0 && <MountsTable entries={mounts} deleteRow={deleteMount} />}\n        </>\n    )\n}\n\nconst MountsTable = ({ entries, deleteRow }: { entries: Mount[]; deleteRow: (name: string) => Promise<void> }) => {\n    const rows = entries.map(entry => ({\n        ...entry,\n        id: entry.name,\n        actions: (\n            <>\n                <a className='btn-flat' onClick={() => deleteRow(entry.name)}>\n                    <m.Icon>delete_forever</m.Icon>\n                </a>\n            </>\n        )\n    }))\n    const columns = [\n        { fieldName: 'name', label: 'Name' },\n        { fieldName: 'path', label: 'Directory path' },\n        { fieldName: 'actions', label: 'Actions' }\n    ]\n\n    return <PaginatedTable columns={columns} rows={rows} />\n}\n\nexport default withRouter(MountsPage)\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/PasswordHealthPage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { withRouter, RouteComponentProps } from 'react-router'\nimport AsyncPasswordHealthCollector, { PasswordHealthCollectionStatus, PasswordSecretHealth } from '../../secrets/AsyncPasswordHealthCollector'\nimport PaginatedTable from '../../components/PaginatedTable'\nimport { LoadingScreen } from '../../components/loading-screen/LoadingScreen'\nimport { PasswordHealthIndicator } from '../password-health/PasswordHealthIndicator'\nimport { PasswordHealthSummary, PasswordRater } from '../password-health/PasswordRater'\n\ninterface PasswordHealthPageState {\n    collector: AsyncPasswordHealthCollector\n    statusChecker?: number\n    status?: PasswordHealthCollectionStatus\n}\n\nclass PasswordHealthPage extends React.Component<RouteComponentProps, PasswordHealthPageState> {\n    constructor(props: any) {\n        super(props)\n        this.state = {\n            collector: new AsyncPasswordHealthCollector()\n        }\n    }\n\n    public async componentDidMount() {\n        const { collector } = this.state\n        collector.start()\n\n        const statusChecker = window.setInterval(async () => {\n            const status = this.state.collector.getCurrentStatus()\n            this.setState({ status })\n            if (!status.inProgress) {\n                await this.stopStatusChecker()\n            }\n        }, 100)\n        this.setState({ statusChecker })\n    }\n\n    public async componentWillUnmount() {\n        await this.stopStatusChecker()\n    }\n\n    public render() {\n        const { status } = this.state\n\n        return (\n            <>\n                <h4>Password Health</h4>\n                {status ? this.renderStatus(status) : <LoadingScreen />}\n            </>\n        )\n    }\n\n    private async stopStatusChecker() {\n        const { collector, statusChecker } = this.state\n        await collector.stopAndReset()\n\n        if (statusChecker) {\n            clearInterval(statusChecker)\n        }\n    }\n\n    // tslint:disable-next-line\n    private renderStatus(status: PasswordHealthCollectionStatus) {\n        if (!status.inProgress && status.passwordsCollected === 0) {\n            return (\n                <p>\n                    It seems you don't have passwords in your stores yet. A secret is considered a password if it contains words such as: password, pw, pass,\n                    secret, key or similar.\n                </p>\n            )\n        }\n\n        if (status.inProgress && status.passwordsCollected > 0) {\n            const progressPercentage = Math.round((status.passwordsCollected / status.totalPasswords) * 100)\n\n            return (\n                <>\n                    <p>Your passwords are currently being collected and analysed, please wait until ready... {progressPercentage}%</p>\n                    <div style={{ width: '60%', minWidth: '200px', marginTop: '30px' }}>\n                        <m.ProgressBar progress={progressPercentage} />\n                    </div>\n                </>\n            )\n        }\n\n        if (!status.inProgress && status.passwordsCollected > 0 && status.passwordsCollected === status.totalPasswords && !status.error) {\n            const overallPasswordHealth = PasswordRater.buildOverallPasswordHealthSummary(status.ratedPasswords)\n            const improvablePasswords = overallPasswordHealth.ratedPasswordSecrets.filter(rated => rated.health && rated.health < 100)\n\n            return (\n                <>\n                    {this.renderOverallPasswordHealth(overallPasswordHealth, improvablePasswords.length)}\n                    {this.renderImprovementPotential(improvablePasswords)}\n                </>\n            )\n        }\n\n        if (!status.inProgress && status.error) {\n            return <p>Something went wrong here: {status.error.message}</p>\n        }\n    }\n\n    private renderOverallPasswordHealth(overallPasswordHealth: PasswordHealthSummary, improvablePasswordsAmount: number) {\n        return (\n            <div className='row'>\n                <div className='col s12'>\n                    <div className='card-panel z-depth-1'>\n                        <div className='row valign-wrapper'>\n                            <div className='col s2'>\n                                <PasswordHealthIndicator health={overallPasswordHealth.health} />\n                            </div>\n                            <div className='col s10'>\n                                This is the average health for your passwords.\n                                {improvablePasswordsAmount > 0 ? ` There are ${improvablePasswordsAmount} suggestions available.` : ''}\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        )\n    }\n\n    private renderImprovementPotential(improvablePasswords: PasswordSecretHealth[]) {\n        return (\n            improvablePasswords.length > 0 && (\n                <>\n                    <h4 className='m-top'>Improvement Potential</h4>\n                    <PaginatedTable\n                        columns={[\n                            { fieldName: 'name', label: 'Name' },\n                            { fieldName: 'health', label: 'Health' },\n                            { fieldName: 'rulesToImprove', label: 'Rules to improve' }\n                        ]}\n                        rows={improvablePasswords.map(rated => ({\n                            id: rated.name,\n                            name: <a onClick={this.onSecretClick(rated.name)}>{rated.name}</a>,\n                            health: `${rated.health}`,\n                            rulesToImprove: `${rated.failedRulesCount}`\n                        }))}\n                    />\n                </>\n            )\n        )\n    }\n\n    private onSecretClick = (secretName: string) => () => this.props.history.replace(`/secret/${btoa(secretName)}`)\n}\n\nexport default withRouter(PasswordHealthPage)\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/SettingsPage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport * as isValidElectronShortcut from 'electron-is-accelerator'\n\nimport { EnvironmentTest } from '../components/EnvironmentTest'\nimport { Settings } from '../../common/Settings'\nimport { ExternalLink } from '../../components/ExternalLink'\n\ninterface SettingsPageState {\n    environmentTestSuccessful: boolean\n    secretValueLength: number\n    searchShortcut: string\n}\n\nexport default function SettingsPage() {\n    const [state, setState] = React.useState<SettingsPageState | undefined>(undefined)\n    const [isShortcutValidationError, setShortcutValidationError] = React.useState<boolean>(false)\n\n    React.useEffect(() => {\n        const { environmentTestSuccessful } = Settings.getSystemSettings()\n        const { secretValueLength, searchShortcut } = Settings.getUserSettings()\n        setState({ environmentTestSuccessful, secretValueLength, searchShortcut })\n    }, [])\n\n    if (!state) {\n        return null\n    }\n\n    return (\n        <>\n            <h4>Settings</h4>\n            <m.CardPanel>\n                <m.Row>\n                    <m.Col s={12}>\n                        <label className='active'>Characters for generated secrets: {state.secretValueLength}</label>\n                        <p style={{ width: '33%' }} className='range-field'>\n                            <input\n                                type='range'\n                                defaultValue={`${state.secretValueLength}`}\n                                min='6'\n                                onChange={event => {\n                                    const value = parseInt(event.target.value, 10)\n                                    Settings.updateUserSettings({ secretValueLength: value })\n                                }}\n                                max='200'\n                            />\n                        </p>\n                    </m.Col>\n                    <m.Input\n                        s={4}\n                        error={isShortcutValidationError ? 'error' : undefined}\n                        defaultValue={state.searchShortcut}\n                        onChange={(_: any, value: string) => {\n                            const isValid = isValidElectronShortcut(value)\n                            if (isValid) {\n                                Settings.updateUserSettings({ searchShortcut: value })\n                            }\n\n                            setShortcutValidationError(!isValid)\n                        }}\n                        label={\n                            <>\n                                <span>Global search window shortcut</span>{' '}\n                                <ExternalLink url='https://www.electronjs.org/docs/api/accelerator#available-modifiers'>(see all options)</ExternalLink>\n                            </>\n                        }\n                    />\n                </m.Row>\n            </m.CardPanel>\n\n            <h4>Environment Test</h4>\n            {state.environmentTestSuccessful && <strong>🙌 The last test was successful 🙌</strong>}\n            <m.CardPanel>\n                <EnvironmentTest />\n            </m.CardPanel>\n        </>\n    )\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/details/HistoryTable.tsx",
    "content": "import * as React from 'react'\nimport * as dateformat from 'dateformat'\n\nimport PaginatedTable from '../../../components/PaginatedTable'\nimport { HistoryEntry } from '../../../secrets/Gopass'\n\nexport interface HistoryTableProps {\n    entries: HistoryEntry[]\n}\n\nexport function HistoryTable({ entries }: HistoryTableProps) {\n    const rows = entries.map(entry => ({\n        ...entry,\n        id: entry.hash,\n        timestamp: dateformat(new Date(entry.timestamp), 'yyyy-mm-dd HH:MM')\n    }))\n    const columns = [\n        { fieldName: 'timestamp', label: 'Time' },\n        { fieldName: 'author', label: 'Author' },\n        { fieldName: 'message', label: 'Message' }\n    ]\n\n    return <PaginatedTable columns={columns} rows={rows} />\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/pages/details/SecretDetailsPage.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'react-router'\n\nimport Gopass, { HistoryEntry } from '../../../secrets/Gopass'\nimport { passwordSecretRegex } from '../../../secrets/deriveIconFromSecretName'\nimport { LoadingScreen } from '../../../components/loading-screen/LoadingScreen'\nimport { useCopySecretToClipboard } from '../../../secrets/useCopySecretToClipboard'\nimport { HistoryTable } from './HistoryTable'\nimport PasswordRatingComponent from '../../password-health/PasswordRatingComponent'\nimport { useSecretsContext } from '../../SecretsProvider'\n\ninterface SecretDetailsPageProps extends RouteComponentProps {\n    secretName: string\n    isAdded?: boolean\n}\n\n// todo: make this configurable in the application settings\nconst DISPLAY_SECRET_VALUE_BY_DEFAULT = false\n\n/* tslint:disable */\nfunction SecretDetailsPage({ secretName, isAdded, history }: SecretDetailsPageProps) {\n    const [secretValue, setSecretValue] = React.useState('')\n    const [historyEntries, setHistoryEntries] = React.useState<HistoryEntry[]>([])\n    const [loading, setLoading] = React.useState(true)\n    const [isPassword, setIsPassword] = React.useState(false)\n    const [displaySecretValue, setDisplaySecretValue] = React.useState(DISPLAY_SECRET_VALUE_BY_DEFAULT)\n    const [editedSecretValue, setEditedSecretValue] = React.useState<string | undefined>(undefined)\n    const [queryDeletion, setQueryDeletion] = React.useState(false)\n\n    React.useEffect(() => {\n        setLoading(true)\n\n        Promise.all([Gopass.show(secretName), Gopass.history(secretName)]).then(([newSecretValue, newHistoryEntries]) => {\n            setSecretValue(newSecretValue)\n            setHistoryEntries(newHistoryEntries)\n            setLoading(false)\n            setQueryDeletion(false)\n            setEditedSecretValue(undefined)\n            setDisplaySecretValue(DISPLAY_SECRET_VALUE_BY_DEFAULT)\n        })\n    }, [secretName])\n\n    React.useEffect(() => {\n        setIsPassword(passwordSecretRegex.test(secretName))\n    }, [secretValue])\n\n    const querySecretDeletion = () => setQueryDeletion(true)\n    const denySecretDeletion = () => setQueryDeletion(false)\n    const confirmSecretDeletion = () => Gopass.deleteSecret(secretName).then(() => history.replace('/'))\n    const deletionModeButtons = queryDeletion ? (\n        <>\n            <a className='link' onClick={denySecretDeletion}>\n                NO, keep it!\n            </a>\n            <a\n                className='link'\n                onClick={async () => {\n                    await confirmSecretDeletion()\n                    await useSecretsContext().reloadSecretNames()\n                }}\n            >\n                Sure!\n            </a>\n        </>\n    ) : (\n        <a className='link' onClick={querySecretDeletion}>\n            Delete\n        </a>\n    )\n\n    const querySecretEditing = () => setEditedSecretValue(secretValue)\n    const discardSecretEditing = () => setEditedSecretValue(undefined)\n    const onNewSecretValueChange = (event: any) => setEditedSecretValue(event.target.value)\n    const saveEditedSecretValue = async () => {\n        if (editedSecretValue && editedSecretValue !== secretValue) {\n            await Gopass.editSecret(secretName, editedSecretValue)\n            setSecretValue(editedSecretValue)\n        }\n\n        setEditedSecretValue(undefined)\n    }\n    const editModeButtons = (\n        <>\n            <a className='link' onClick={discardSecretEditing}>\n                Discard\n            </a>\n            <a className='link' onClick={saveEditedSecretValue}>\n                Save changes\n            </a>\n        </>\n    )\n\n    const inEditMode = editedSecretValue !== undefined\n    const copySecretToClipboard = useCopySecretToClipboard()\n    const cardActions = [\n        <a key='toggle-display' className='link' onClick={() => setDisplaySecretValue(!displaySecretValue)}>\n            {displaySecretValue ? 'Hide' : 'Show'}\n        </a>,\n        <a key='copy-clipboard' className='link' onClick={() => copySecretToClipboard(secretName)}>\n            Copy to clipboard\n        </a>,\n        inEditMode ? (\n            <span key='edit-secret-mode-actions'>{editModeButtons}</span>\n        ) : (\n            <span key='view-secret-mode-actions'>\n                <a className='link' onClick={querySecretEditing}>\n                    Edit\n                </a>\n                {deletionModeButtons}\n            </span>\n        )\n    ]\n    const secretValueToDisplay = displaySecretValue ? secretValue : '*******************'\n    const valueToDisplay = inEditMode ? editedSecretValue : secretValueToDisplay\n    const linesRequired = (valueToDisplay || '').split('\\n').length\n\n    return loading ? (\n        <>\n            <h4>Secret {isAdded && <m.Icon small>fiber_new</m.Icon>}</h4>\n            <LoadingScreen />\n        </>\n    ) : (\n        <>\n            <h4>Secret {isAdded && <m.Icon small>fiber_new</m.Icon>}</h4>\n            <m.Card title={secretName} actions={cardActions}>\n                <textarea\n                    style={{\n                        color: '#212121',\n                        fontSize: 16,\n                        borderBottom: '1px dotted rgba(0, 0, 0, 0.42)',\n                        height: 21 * linesRequired,\n                        borderTop: 'none',\n                        borderRight: 'none',\n                        borderLeft: 'none'\n                    }}\n                    value={valueToDisplay}\n                    disabled={!inEditMode}\n                    onChange={onNewSecretValueChange}\n                    ref={input => input && input.focus()}\n                />\n            </m.Card>\n\n            {isPassword && (\n                <>\n                    <h4 className='m-top'>Password Strength</h4>\n                    <PasswordRatingComponent secretValue={secretValue} />\n                </>\n            )}\n\n            <h4 className='m-top'>History</h4>\n            <HistoryTable entries={historyEntries} />\n        </>\n    )\n}\n\nexport default withRouter(SecretDetailsPage)\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordHealthIndicator.tsx",
    "content": "import * as React from 'react'\n\nexport const passwordStrengthColorExtractor = (health: number): string => {\n    if (health >= 70) {\n        return 'green'\n    }\n    if (health >= 50) {\n        return 'yellow'\n    }\n\n    return 'red'\n}\n\nexport const PasswordHealthIndicator = ({ health }: { health: number }) => (\n    <div className={`password-strength-sum ${passwordStrengthColorExtractor(health)}`}>\n        <span>{health}</span>\n    </div>\n)\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordHealthRules.ts",
    "content": "import { PasswordHealthRule } from './PasswordRule'\n\nconst minimumLengthRule: PasswordHealthRule = {\n    matcher: (password: string) => password.length >= 10,\n    name: 'Minimal length of 10 characters',\n    description: 'Good passwords should have a minimum of 10 characters'\n}\n\nconst lowerCaseRule: PasswordHealthRule = {\n    matcher: (password: string) => /(?=.*[a-z])/.test(password),\n    name: 'Lowercase letters',\n    description: 'Make sure to have a least one lowercase letter inside'\n}\n\nconst upperCaseRule: PasswordHealthRule = {\n    matcher: (password: string) => /(?=.*[A-Z])/.test(password),\n    name: 'Uppercase letters',\n    description: 'Make sure it contains at least one uppercase letter'\n}\n\nconst specialCharRule: PasswordHealthRule = {\n    matcher: (password: string) => /(?=.*\\W)/.test(password),\n    name: 'Special characters',\n    description: 'Use special characters to make your password stronger'\n}\n\nconst numbersRule: PasswordHealthRule = {\n    matcher: (password: string) => /(?=.*\\d)/.test(password),\n    name: 'Numbers',\n    description: 'Make sure to use at least one number to improve your password'\n}\n\nconst noRepetitiveCharactersRule: PasswordHealthRule = {\n    matcher: (password: string) => !!password && !/(.)\\1{2,}/.test(password),\n    name: 'No repetitive characters',\n    description: 'Passwords are better if characters are not repeated often (sequence of three or more).'\n}\n\nexport const allPasswordHealthRules: PasswordHealthRule[] = [\n    minimumLengthRule,\n    lowerCaseRule,\n    upperCaseRule,\n    specialCharRule,\n    numbersRule,\n    noRepetitiveCharactersRule\n]\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRater.ts",
    "content": "import { PasswordHealthRule, PasswordHealthRuleInfo } from './PasswordRule'\nimport { allPasswordHealthRules } from './PasswordHealthRules'\nimport { PasswordSecretHealth } from '../../secrets/AsyncPasswordHealthCollector'\n\nexport interface PasswordRatingResult {\n    totalRulesCount: number\n    failedRules: PasswordHealthRuleInfo[]\n    health: number\n}\n\nconst ruleToMeta = (rule: PasswordHealthRule): PasswordHealthRuleInfo => ({ name: rule.name, description: rule.description })\nconst calculateHealth = (failed: number, total: number) => Math.round(100 - (failed / total) * 100)\n\nexport interface PasswordHealthSummary {\n    health: number\n    ratedPasswordSecrets: PasswordSecretHealth[]\n}\n\nexport class PasswordRater {\n    public static ratePassword(password: string): PasswordRatingResult {\n        const totalRulesCount = allPasswordHealthRules.length\n        const failedRules = allPasswordHealthRules.filter(rule => !rule.matcher(password)).map(ruleToMeta)\n\n        return {\n            totalRulesCount,\n            failedRules,\n            health: calculateHealth(failedRules.length, totalRulesCount)\n        }\n    }\n\n    public static buildOverallPasswordHealthSummary(passwordHealths: PasswordSecretHealth[]): PasswordHealthSummary {\n        const pwHealthSum = passwordHealths.map(h => h.health).reduce((a: number, b: number) => a + b, 0)\n\n        return {\n            health: Math.round(pwHealthSum / passwordHealths.length),\n            ratedPasswordSecrets: passwordHealths\n        }\n    }\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRatingComponent.css",
    "content": "ol.failed-rules-list {\n    padding-left: 17px;\n}\n\nol.failed-rules-list > li > strong {\n    font-weight: 500;\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRatingComponent.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport { PasswordRater } from './PasswordRater'\nimport { PasswordHealthRuleInfo } from './PasswordRule'\nimport { PasswordHealthIndicator } from './PasswordHealthIndicator'\n\nimport './PasswordRatingComponent.css'\n\nexport interface PasswordRatingComponentProps {\n    secretValue: string\n}\n\nexport const FailedRulesList = ({ failedRules }: { failedRules: PasswordHealthRuleInfo[] }) => (\n    <>\n        {failedRules.length > 0 ? (\n            <>\n                <p>\n                    You could improve <strong>{failedRules.length}</strong> characteristica of this password.\n                </p>\n                <ol className='failed-rules-list'>\n                    {failedRules.map((failedRule: PasswordHealthRuleInfo, index: number) => (\n                        <li key={index}>\n                            <strong>{failedRule.name}:</strong> {failedRule.description}\n                        </li>\n                    ))}\n                </ol>\n            </>\n        ) : (\n            <p>Good job! This secret satisfies all basic criteria for a potentially good password.</p>\n        )}\n    </>\n)\n\nconst PasswordRatingComponent = ({ secretValue }: PasswordRatingComponentProps) => {\n    const { health, failedRules } = PasswordRater.ratePassword(secretValue)\n\n    return (\n        <m.Row>\n            <m.Col s={12}>\n                <div className='card-panel z-depth-1'>\n                    <div className='row valign-wrapper'>\n                        <div className='col s2'>\n                            <PasswordHealthIndicator health={health} />\n                        </div>\n                        <div className='col s10'>\n                            <FailedRulesList failedRules={failedRules} />\n                        </div>\n                    </div>\n                </div>\n            </m.Col>\n        </m.Row>\n    )\n}\n\nexport default PasswordRatingComponent\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRule.d.ts",
    "content": "export interface PasswordHealthRuleInfo {\n    name: string\n    description: string\n}\n\nexport interface PasswordHealthRule extends PasswordHealthRuleInfo {\n    matcher: (password: string) => boolean\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretExplorer.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport * as KeyboardEventHandler from 'react-keyboard-event-handler'\n\nimport { RouteComponentProps, withRouter } from 'react-router'\nimport SecretTree from './SecretTree'\nimport { useSecretsContext } from '../SecretsProvider'\n\nconst SecretExplorer = ({ history }: RouteComponentProps) => {\n    const { tree, applySearchToTree, reloadSecretNames, searchValue } = useSecretsContext()\n    const navigateToSecretDetailView = (secretName: string) => history.replace(`/secret/${btoa(secretName)}`)\n    const clearSearch = () => applySearchToTree('')\n\n    React.useEffect(() => {\n        reloadSecretNames()\n    }, [])\n\n    return (\n        <div className='secret-explorer'>\n            <KeyboardEventHandler handleKeys={['esc']} handleFocusableElements onKeyEvent={clearSearch} />\n            <div className='search-bar'>\n                <m.Input\n                    value={searchValue}\n                    placeholder='Search...'\n                    onChange={(_: any, updatedSearchValue: string) => {\n                        applySearchToTree(updatedSearchValue)\n                    }}\n                />\n            </div>\n            <SecretTree tree={tree} onSecretClick={navigateToSecretDetailView} />\n        </div>\n    )\n}\n\nexport default withRouter(SecretExplorer)\n"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretTree.tsx",
    "content": "import * as React from 'react'\nimport TreeComponent, { Tree } from '../../components/tree/TreeComponent'\n\nexport interface SecretTreeViewerProps {\n    onSecretClick: (name: string) => void\n    tree: Tree\n}\n\nexport default class SecretTreeViewer extends React.Component<SecretTreeViewerProps, {}> {\n    public render() {\n        return <TreeComponent tree={this.props.tree} onLeafClick={this.props.onSecretClick} />\n    }\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretsDirectoryService.ts",
    "content": "import { Tree } from '../../components/tree/TreeComponent'\n\nexport default class SecretsDirectoryService {\n    public static secretPathsToTree(secretPaths: string[], previousTree: Tree, openAllEntries: boolean): Tree {\n        const directory = SecretsDirectoryService.secretPathsToDirectory(secretPaths)\n        return SecretsDirectoryService.directoryToTree(directory, previousTree, openAllEntries)\n    }\n\n    /**\n     * Convert\n     *  from: \"xyz/service/someServiceName/db/password\"\n     *  to: \"{ xyz: { service: { someServiceName: { db: { password: {} } } } }  }\"\n     */\n    private static secretPathsToDirectory(secretPaths: string[]): any {\n        const directory: { [key: string]: any } = {}\n\n        for (const secretPath of secretPaths) {\n            const segments = secretPath.split('/')\n\n            let tempDir = directory\n            segments.forEach((segment: string, index: number) => {\n                if (!tempDir[segment]) {\n                    tempDir[segment] = index + 1 === segments.length ? secretPath : {}\n                }\n\n                tempDir = tempDir[segment]\n            })\n        }\n\n        return directory\n    }\n\n    private static getToggledPathsFromTree(tree: Tree): string[] {\n        const paths: string[] = []\n\n        for (const child of tree.children || []) {\n            if (child.toggled) {\n                paths.push(child.path)\n            }\n            paths.push(...SecretsDirectoryService.getToggledPathsFromTree(child))\n        }\n\n        return paths\n    }\n\n    /**\n     * Convert\n     *  from: \"{ xyz: { service: { someServiceName: { db: { password: {} } } } }  }\"\n     *  to Tree interface\n     */\n    private static directoryToTree(directory: any, previousTree: Tree, openAllEntries: boolean): Tree {\n        const toggledPaths = SecretsDirectoryService.getToggledPathsFromTree(previousTree)\n        const children = SecretsDirectoryService.getChildren(directory, toggledPaths, true, openAllEntries)\n        const tree: Tree = {\n            name: 'Stores',\n            toggled: true,\n            path: '',\n            children\n        }\n\n        return tree\n    }\n\n    private static getChildren(\n        directory: any | string,\n        toggledPaths: string[],\n        toggled: boolean = false,\n        toggleAll: boolean = false,\n        parentPath = ''\n    ): Tree[] | undefined {\n        if (!(directory instanceof Object)) {\n            return undefined\n        }\n        const childDirNames = Object.keys(directory).filter(key => key !== '')\n\n        if (childDirNames.length === 0) {\n            return undefined\n        }\n\n        return childDirNames.map(name => {\n            const path = parentPath.length > 0 ? parentPath + '/' + name : name\n            const toggledFromPreviousTree = toggledPaths.includes(path)\n            const toggledPathsLeft = toggledFromPreviousTree ? toggledPaths.filter(p => p !== path) : toggledPaths\n            const children = SecretsDirectoryService.getChildren(directory[name], toggledPathsLeft, false, toggleAll, path)\n\n            return {\n                name,\n                path,\n                children,\n                toggled: toggleAll || toggled || toggledFromPreviousTree\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretsFilterService.ts",
    "content": "export default class SecretsFilterService {\n    public static filterBySearch(secretNames: string[], searchValue: string): string[] {\n        const searchValues = searchValue\n            .split(' ')\n            .map(value => value.trim())\n            .filter(value => value !== '')\n\n        return secretNames.filter(SecretsFilterService.filterMatchingSecrets(searchValues))\n    }\n\n    private static filterMatchingSecrets = (searchValues: string[]) => (secretName: string) => {\n        if (searchValues.length > 0) {\n            return searchValues.every(value => secretName.includes(value))\n        }\n\n        return true\n    }\n}\n"
  },
  {
    "path": "src/renderer/explorer-app.tsx",
    "content": "import * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport { HashRouter as Router } from 'react-router-dom'\nimport { AppContainer } from 'react-hot-loader'\n\nimport ExplorerApplication from './explorer-app/ExplorerApplication'\n\nimport 'materialize-css/dist/css/materialize.css'\nimport 'material-design-icons/iconfont/material-icons.css'\nimport 'animate.css/animate.css'\nimport { SecretsProvider } from './explorer-app/SecretsProvider'\n\nconst mainElement = document.createElement('div')\ndocument.body.appendChild(mainElement)\n\nReactDOM.render(\n    <AppContainer>\n        <Router>\n            <SecretsProvider>\n                <ExplorerApplication />\n            </SecretsProvider>\n        </Router>\n    </AppContainer>,\n    mainElement\n)\n"
  },
  {
    "path": "src/renderer/search-app/CollectionItems.tsx",
    "content": "import * as m from 'react-materialize'\nimport * as React from 'react'\n\nimport { SecretText } from './SecretText'\n\nexport interface CollectionItemProps {\n    filteredSecretNames: string[]\n    selectedItemIndex: number\n    highlightRegExp?: RegExp\n    onItemClick: (secretKey: string) => void\n}\n\nexport function CollectionItems({ filteredSecretNames, selectedItemIndex, onItemClick, highlightRegExp }: CollectionItemProps) {\n    return (\n        <>\n            {filteredSecretNames.map((secretKey, i) => {\n                const secretPath = secretKey.split('/')\n                const isSelected = i === selectedItemIndex ? 'selected' : undefined\n\n                return (\n                    <m.CollectionItem key={`entry-${i}`} className={isSelected} onClick={() => onItemClick(secretKey)}>\n                        <SecretText secretPath={secretPath} highlightRegExp={highlightRegExp} />\n                    </m.CollectionItem>\n                )\n            })}\n        </>\n    )\n}\n"
  },
  {
    "path": "src/renderer/search-app/SearchApplication.css",
    "content": "strong {\n    font-weight: bolder;\n}\n\n.collection .collection-item:hover {\n    background-color: #EBEBEB;\n}\n\n.collection .collection-item.selected {\n    background-color: #DCDCDC;\n}\n\n.collection .collection-item {\n    cursor: pointer;\n}\n\n.link {\n    cursor: pointer\n}\n"
  },
  {
    "path": "src/renderer/search-app/SearchApplication.tsx",
    "content": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport SearchResults from './SearchResults'\nimport Notification from '../common/notifications/Notification'\nimport NotificationProvider from '../common/notifications/NotificationProvider'\n\nimport './SearchApplication.css'\n\nconst NAVIGATION_KEYS = ['ArrowUp', 'ArrowDown', 'Enter', 'Tab']\n\nconst preventNavigationKeys = (event: any) => {\n    if (NAVIGATION_KEYS.includes(event.key)) {\n        event.preventDefault()\n    }\n}\n\nexport function SearchApplication() {\n    const [searchValue, setSearchValue] = React.useState('')\n\n    React.useEffect(() => {\n        const element = document.getElementById('search')\n\n        if (element) {\n            element.focus()\n            element.click()\n        }\n    }, [])\n\n    const onChange = (_: any, newValue: string) => {\n        setSearchValue(newValue)\n    }\n\n    return (\n        <NotificationProvider>\n            <Notification dismissTimeout={3000} />\n            <m.Row>\n                <m.Col s={12}>\n                    <m.Input id='search' placeholder='Search...' onChange={onChange} onKeyDown={preventNavigationKeys} s={12} />\n                </m.Col>\n            </m.Row>\n            <m.Row>\n                <m.Col s={12}>\n                    <SearchResults search={searchValue} />\n                </m.Col>\n            </m.Row>\n        </NotificationProvider>\n    )\n}\n"
  },
  {
    "path": "src/renderer/search-app/SearchResults.tsx",
    "content": "import * as React from 'react'\nimport { ipcRenderer } from 'electron'\nimport * as m from 'react-materialize'\nimport * as KeyboardEventHandler from 'react-keyboard-event-handler'\n\nimport Gopass from '../secrets/Gopass'\nimport { useCopySecretToClipboard } from '../secrets/useCopySecretToClipboard'\nimport { CollectionItems } from './CollectionItems'\n\nconst NUMBER_OF_SEARCH_RESULTS = 15\n\nexport interface SearchResultsProps {\n    search: string\n}\n\nexport default function SearchResults(props: SearchResultsProps) {\n    const [allSecretNames, setAllSecretNames] = React.useState<string[]>([])\n    const [filteredSecretNames, setFilteredSecretNames] = React.useState<string[]>([])\n    const [selectedItemIndex, setSelectedItemIndex] = React.useState(0)\n    const [highlightRegExp, setHighlightRegExp] = React.useState<RegExp | undefined>()\n\n    const copySecretToClipboard = useCopySecretToClipboard()\n\n    const updateFilteredSecrets = (search?: string) => {\n        if (search) {\n            const searchValues = search.split(' ').map(searchValue => searchValue.trim())\n\n            setFilteredSecretNames(allSecretNames.filter(filterMatchingSecrets(searchValues)).slice(0, NUMBER_OF_SEARCH_RESULTS))\n            setHighlightRegExp(new RegExp(`(${searchValues.join('|')})`, 'g'))\n        } else {\n            setFilteredSecretNames(allSecretNames.slice(0, NUMBER_OF_SEARCH_RESULTS))\n            setHighlightRegExp(undefined)\n        }\n        setSelectedItemIndex(0)\n    }\n\n    React.useEffect(() => {\n        Gopass.getAllSecretNames().then(newSecretNames => {\n            setAllSecretNames(newSecretNames)\n        })\n    }, [])\n\n    React.useEffect(() => {\n        updateFilteredSecrets()\n    }, [allSecretNames])\n\n    React.useEffect(() => {\n        updateFilteredSecrets(props.search)\n    }, [props.search])\n\n    const onKeyEvent = (key: string, event: any) => {\n        switch (key) {\n            case 'shift+tab':\n            case 'up':\n                if (selectedItemIndex > 0) {\n                    setSelectedItemIndex(selectedItemIndex - 1)\n                    event.preventDefault()\n                }\n                break\n\n            case 'down':\n            case 'tab':\n                if (selectedItemIndex < filteredSecretNames.length - 1) {\n                    setSelectedItemIndex(selectedItemIndex + 1)\n                    event.preventDefault()\n                }\n                break\n\n            case 'enter':\n                const secretKey = filteredSecretNames[selectedItemIndex]\n                if (secretKey) {\n                    copySecretToClipboard(secretKey)\n                }\n\n                event.preventDefault()\n                break\n\n            case 'esc':\n                ipcRenderer.send('hideSearchWindow')\n                break\n            default:\n                console.error('This should not happen ;-) Please verify the \"handleKeys\" prop from the \"KeyboardEventHandler\"')\n                break\n        }\n    }\n\n    const onSelectCollectionItem = (secretKey: string) => () => {\n        copySecretToClipboard(secretKey)\n    }\n\n    return (\n        <>\n            <KeyboardEventHandler handleKeys={['up', 'shift+tab', 'down', 'tab', 'enter', 'esc']} handleFocusableElements onKeyEvent={onKeyEvent} />\n            <m.Collection>\n                <CollectionItems\n                    filteredSecretNames={filteredSecretNames}\n                    selectedItemIndex={selectedItemIndex}\n                    highlightRegExp={highlightRegExp}\n                    onItemClick={onSelectCollectionItem}\n                />\n            </m.Collection>\n        </>\n    )\n}\n\nfunction filterMatchingSecrets(searchValues: string[]) {\n    return (secretName: string) => searchValues.every(searchValue => secretName.toLowerCase().includes(searchValue.toLowerCase()))\n}\n"
  },
  {
    "path": "src/renderer/search-app/SearchResultsView.tsx",
    "content": ""
  },
  {
    "path": "src/renderer/search-app/SecretText.tsx",
    "content": "import * as React from 'react'\nimport * as replace from 'string-replace-to-array'\n\nexport interface SecretTextProps {\n    secretPath: string[]\n    highlightRegExp?: RegExp\n}\n\nconst getHighlightedSegment = (segment: string, highlightRegExp?: RegExp) => {\n    if (!highlightRegExp) {\n        return segment\n    }\n\n    return replace(segment, highlightRegExp, (_: any, match: string, offset: number) => {\n        return <mark key={`highlight-${segment}-${offset}`}>{match}</mark>\n    })\n}\n\nexport function SecretText({ secretPath, highlightRegExp }: SecretTextProps) {\n    return (\n        <>\n            {secretPath.reduce((result: string[], segment, currentIndex) => {\n                const extendedResult = result.concat(getHighlightedSegment(segment, highlightRegExp))\n\n                if (currentIndex < secretPath.length - 1) {\n                    extendedResult.push(' > ')\n                }\n\n                return extendedResult\n            }, [])}\n        </>\n    )\n}\n"
  },
  {
    "path": "src/renderer/search-app.tsx",
    "content": "import * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport { AppContainer } from 'react-hot-loader'\n\nimport { SearchApplication } from './search-app/SearchApplication'\n\nimport 'materialize-css/dist/css/materialize.css'\nimport 'material-design-icons/iconfont/material-icons.css'\n\nconst mainElement = document.createElement('div')\ndocument.body.appendChild(mainElement)\n\nReactDOM.render(\n    <AppContainer>\n        <SearchApplication />\n    </AppContainer>,\n    mainElement\n)\n"
  },
  {
    "path": "src/renderer/secrets/AsyncPasswordHealthCollector.ts",
    "content": "import Gopass from './Gopass'\nimport { sortBy } from 'lodash'\nimport { passwordSecretRegex } from './deriveIconFromSecretName'\nimport { PasswordRater } from '../explorer-app/password-health/PasswordRater'\n\nexport interface PasswordSecretHealth {\n    name: string\n    health: number\n    failedRulesCount: number\n}\n\nexport interface PasswordHealthCollectionStatus {\n    totalPasswords: number\n    passwordsCollected: number\n    inProgress: boolean\n    ratedPasswords: PasswordSecretHealth[]\n    error: Error | undefined\n}\n\nexport default class AsyncPasswordHealthCollector {\n    private status: PasswordHealthCollectionStatus = {\n        totalPasswords: 0,\n        passwordsCollected: 0,\n        inProgress: false,\n        ratedPasswords: [],\n        error: undefined\n    }\n\n    public async start() {\n        const passwordSecretNames = await this.initializePasswordSecretNames(true)\n\n        try {\n            await this.collectPasswordHealthOneAfterAnother(passwordSecretNames)\n            this.status.inProgress = false\n        } catch (e) {\n            this.status.error = e\n            this.status.inProgress = false\n        }\n    }\n\n    public async stopAndReset() {\n        await this.initializePasswordSecretNames(false)\n    }\n\n    public getCurrentStatus(): PasswordHealthCollectionStatus {\n        this.status.ratedPasswords = sortBy(this.status.ratedPasswords, (rp: PasswordSecretHealth) => rp.health)\n        return this.status\n    }\n\n    private async initializePasswordSecretNames(inProgress: boolean): Promise<string[]> {\n        this.status.inProgress = inProgress\n        const passwordSecretNames = await this.getAllSecretsContainingPasswords()\n        this.status.ratedPasswords = []\n        this.status.error = undefined\n        this.status.totalPasswords = passwordSecretNames.length\n\n        return passwordSecretNames\n    }\n\n    private async getAllSecretsContainingPasswords(): Promise<string[]> {\n        const allSecretNames = await Gopass.getAllSecretNames()\n        return allSecretNames.filter(secretName => passwordSecretRegex.test(secretName))\n    }\n\n    private collectPasswordHealthOneAfterAnother = async (passwordSecretNames: string[]) => {\n        this.status.ratedPasswords = []\n\n        // use for-loop to do only one Gopass lookup at a time\n        for (const name of passwordSecretNames) {\n            if (this.status.inProgress) {\n                const value = await Gopass.show(name)\n                const { health, failedRules } = PasswordRater.ratePassword(value)\n\n                this.status.ratedPasswords.push({ name, health, failedRulesCount: failedRules.length })\n                this.status.passwordsCollected++\n            } else {\n                return\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/renderer/secrets/Gopass.ts",
    "content": "import { ipcRenderer } from 'electron'\n\nexport interface HistoryEntry {\n    hash: string\n    author: string\n    timestamp: string\n    message: string\n}\n\nexport interface Mount {\n    name: string\n    path: string\n}\n\nconst lineSplitRegex = /\\r?\\n/\nconst isDefined = (value: string) => !!value\nconst escapeShellValue = (value: string) => value.replace(/([\"'$`\\\\])/g, '\\\\$1')\n\nexport default class Gopass {\n    public static executionId = 1\n\n    public static async copy(key: string): Promise<string> {\n        return Gopass.execute(`show -c \"${escapeShellValue(key)}\"`)\n    }\n\n    public static async show(key: string): Promise<string> {\n        return Gopass.execute(`show -f \"${escapeShellValue(key)}\"`)\n    }\n\n    public static async history(key: string): Promise<HistoryEntry[]> {\n        try {\n            return (await Gopass.execute(`history \"${escapeShellValue(key)}\"`))\n                .split(lineSplitRegex)\n                .filter(isDefined)\n                .map(historyLine => {\n                    const lineSplit = historyLine.split(' - ')\n\n                    return {\n                        hash: lineSplit[0],\n                        author: lineSplit[1],\n                        timestamp: lineSplit[2],\n                        message: lineSplit[3]\n                    }\n                })\n        } catch {\n            return []\n        }\n    }\n\n    public static async getMyRecipientId(): Promise<string | undefined> {\n        const recipientIdLine = (await Gopass.execute('recipients')).split(lineSplitRegex).find(line => line.includes('└──'))\n        if (recipientIdLine) {\n            const lineSplit = recipientIdLine.split(' ')\n            return lineSplit[4].replace('0x', '')\n        }\n    }\n\n    public static async getAllMounts(): Promise<Mount[]> {\n        try {\n            return (await Gopass.execute('mounts'))\n                .split(lineSplitRegex)\n                .filter(line => line.includes('└──') || line.includes('├──'))\n                .map(mountLine => {\n                    const lineSplit = mountLine.split(' ')\n\n                    return {\n                        name: lineSplit[1],\n                        path: lineSplit[2].replace(/[{()}]/g, '')\n                    }\n                })\n        } catch {\n            return []\n        }\n    }\n\n    public static async addMount(mount: Mount) {\n        const myRecipientId = await Gopass.getMyRecipientId()\n        if (myRecipientId) {\n            const result = await Gopass.execute('mounts add', [\n                `\"${escapeShellValue(mount.name)}\"`,\n                `\"${escapeShellValue(mount.path)}\"`,\n                `-i \"${myRecipientId}\"`\n            ])\n            if (result.includes('is already mounted')) {\n                // tslint:disable-next-line\n                throw 'duplicate-name'\n            }\n        } else {\n            throw new Error('Own GPG recipient ID could not be determined')\n        }\n    }\n\n    public static async deleteMount(name: string) {\n        await Gopass.execute('mounts rm', [`\"${escapeShellValue(name)}\"`])\n    }\n\n    public static async sync(): Promise<void> {\n        await Gopass.execute('sync')\n    }\n\n    public static async getAllSecretNames(): Promise<string[]> {\n        const flatSecrets = await Gopass.execute('list', ['--flat'])\n\n        return flatSecrets.split(lineSplitRegex).filter(isDefined)\n    }\n\n    public static async addSecret(name: string, value: string): Promise<void> {\n        await Gopass.execute('insert', [`\"${escapeShellValue(name.trim())}\"`], escapeShellValue(value))\n    }\n\n    public static async editSecret(name: string, newValue: string): Promise<void> {\n        await Gopass.execute('insert', ['--force', `\"${escapeShellValue(name.trim())}\"`], escapeShellValue(newValue))\n    }\n\n    public static async deleteSecret(name: string): Promise<void> {\n        await Gopass.execute('rm', ['--force', `\"${escapeShellValue(name)}\"`])\n    }\n\n    private static execute(command: string, args?: string[], pipeTextInto?: string): Promise<string> {\n        // tslint:disable-next-line\n\n        const result = new Promise<string>((resolve, reject) => {\n            ipcRenderer.once(`gopass-answer-${Gopass.executionId}`, (_: Event, value: any) => {\n                if (value.status === 'ERROR') {\n                    reject(value.payload)\n                } else {\n                    resolve(value.payload)\n                }\n            })\n        })\n\n        ipcRenderer.send('gopass', { executionId: Gopass.executionId, command, args, pipeTextInto })\n\n        Gopass.executionId++\n\n        return result\n    }\n}\n"
  },
  {
    "path": "src/renderer/secrets/deriveIconFromSecretName.ts",
    "content": "export interface SecretIconMapping {\n    regex: RegExp\n    icon: string\n}\n\nexport const passwordSecretRegex: RegExp = /(password|pw|pass|secret|key$|passphrase|certificate)/\n\nconst secretIconMappings: SecretIconMapping[] = [\n    {\n        regex: passwordSecretRegex,\n        icon: 'lock'\n    },\n    {\n        regex: /(user|name|id)/,\n        icon: 'person'\n    },\n    {\n        regex: /(note|comment|misc)/,\n        icon: 'comment'\n    },\n    {\n        regex: /(uri|url|link|connection)/,\n        icon: 'filter_center_focus'\n    }\n]\n\nexport const deriveIconFromSecretName = (secretName: string) => {\n    let iconType = 'comment'\n\n    secretIconMappings.forEach((mapping: SecretIconMapping) => {\n        const hasIconMapping = mapping.regex.test(secretName)\n\n        if (hasIconMapping) {\n            iconType = mapping.icon\n        }\n    })\n\n    return iconType\n}\n"
  },
  {
    "path": "src/renderer/secrets/useCopySecretToClipboard.ts",
    "content": "import { ipcRenderer } from 'electron'\n\nimport Gopass from '../secrets/Gopass'\nimport { useNotificationContext } from '../common/notifications/NotificationProvider'\n\nexport function useCopySecretToClipboard() {\n    const notificationContext = useNotificationContext()\n\n    return (secretKey: string) => {\n        Gopass.copy(secretKey)\n            .then(() => {\n                notificationContext.show({ status: 'OK', message: 'Secret has been copied to your clipboard.' })\n                ipcRenderer.send('hideSearchWindow')\n            })\n            .catch(() => {\n                notificationContext.show({ status: 'ERROR', message: 'Oops, something went wrong. Please try again.' })\n            })\n    }\n}\n"
  },
  {
    "path": "src/renderer/types/electron-is-accelerator.d.ts",
    "content": "declare module 'electron-is-accelerator' {\n    // tslint:disable-next-line\n    function isValidElectronShortcut(shortcut: string): boolean\n\n    namespace isValidElectronShortcut {}\n    export = isValidElectronShortcut\n}\n"
  },
  {
    "path": "src/renderer/types/fallback.d.ts",
    "content": "declare module 'react-materialize' {\n    const x: any\n    export = x\n}\n\ndeclare module 'react-treebeard' {\n    const x: any\n    export = x\n}\n\ndeclare module '@emotion/styled' {\n    const x: any\n    export = x\n}\n\ndeclare module 'react-keyboard-event-handler' {\n    const x: any\n    export = x\n}\n\ndeclare module 'fix-path' {\n    const x: any\n    export = x\n}\n"
  },
  {
    "path": "src/renderer/types/promise-timeout.d.ts",
    "content": "declare module 'promise-timeout' {\n    namespace promiseTimeout {\n        class TimeoutError extends Error {}\n\n        function timeout<T>(promise: Promise<T>, timeoutMillis: number): Promise<T>\n    }\n\n    export = promiseTimeout\n}\n"
  },
  {
    "path": "src/renderer/types/string-replace-to-array.d.ts",
    "content": "declare module 'string-replace-to-array' {\n    // tslint:disable-next-line\n    function replace(haystack: string, needle: RegExp | string, newVal: string | Function): any[]\n\n    namespace replace {}\n    export = replace\n}\n"
  },
  {
    "path": "src/shared/settings.ts",
    "content": "export interface UserSettings {\n    secretValueLength: number\n    searchShortcut: string\n    showTray: boolean\n    startOnLogin: boolean\n}\n\nexport const DEFAULT_USER_SETTINGS: UserSettings = {\n    secretValueLength: 50,\n    searchShortcut: 'CmdOrCtrl+Shift+p',\n    showTray: true,\n    startOnLogin: true\n}\n\nexport interface SystemSettings {\n    environmentTestSuccessful: boolean\n    releaseCheckedTimestamp?: number\n    releaseCheck: {\n        lastCheckTimestamp?: number\n        results?: any\n    }\n}\n\nexport const DEFAULT_SYSTEM_SETTINGS: SystemSettings = {\n    environmentTestSuccessful: false,\n    releaseCheck: {}\n}\n"
  },
  {
    "path": "test/Gopass.test.ts",
    "content": "import { IpcMainEvent } from 'electron'\nimport Gopass from '../src/renderer/secrets/Gopass'\nimport { ipcMain, ipcRenderer } from './mock/electron-mock'\n\nconst mockGopassResponse = (payload: string) => {\n    ipcMain.once('gopass', (event: IpcMainEvent) => {\n        event.sender.send('gopass-answer-1', {\n            status: 'OK',\n            executionId: 1,\n            payload\n        })\n    })\n    Gopass.executionId = 1\n}\n\ndescribe('Gopass', () => {\n    const ipcRendererSendSpy = jest.spyOn(ipcRenderer, 'send')\n    beforeEach(() => {\n        jest.clearAllMocks()\n    })\n\n    describe('show', () => {\n        it('should call Gopass correctly', async () => {\n            mockGopassResponse('someValue')\n            await Gopass.show('some-secret-name')\n\n            expect(ipcRendererSendSpy).toHaveBeenCalledWith('gopass', {\n                args: undefined,\n                command: 'show -f \"some-secret-name\"',\n                executionId: 1,\n                pipeTextInto: undefined\n            })\n        })\n\n        it('should call Gopass with escaped $,\",\\',` and \\\\ in secret names', async () => {\n            mockGopassResponse('someValue')\n            await Gopass.show('not nice $ \" \\' ` secret name\"')\n\n            expect(ipcRendererSendSpy).toHaveBeenCalledWith('gopass', {\n                args: undefined,\n                command: 'show -f \"not nice \\\\$ \\\\\" \\\\\\' \\\\` secret name\\\\\"\"',\n                executionId: 1,\n                pipeTextInto: undefined\n            })\n        })\n\n        it('should deliver the secret value', async () => {\n            mockGopassResponse('someValue')\n            const secretValue = await Gopass.show('some-secret-name')\n            expect(secretValue).toBe('someValue')\n        })\n    })\n\n    describe('copy', () => {\n        it('should call Gopass correctly', async () => {\n            mockGopassResponse('someValue')\n            await Gopass.copy('some-secret-name')\n\n            expect(ipcRendererSendSpy).toHaveBeenCalledWith('gopass', {\n                args: undefined,\n                command: 'show -c \"some-secret-name\"',\n                executionId: 1,\n                pipeTextInto: undefined\n            })\n        })\n\n        it('should deliver the secret value', async () => {\n            mockGopassResponse('someValue')\n            const secretValue = await Gopass.copy('some-secret-name')\n            expect(secretValue).toBe('someValue')\n        })\n    })\n})\n"
  },
  {
    "path": "test/PasswordHealthIndicator.test.ts",
    "content": "import { passwordStrengthColorExtractor } from '../src/renderer/explorer-app/password-health/PasswordHealthIndicator'\n\ndescribe('PasswordHealthIndicator', () => {\n    it.each`\n        health | color\n        ${70}  | ${'green'}\n        ${100} | ${'green'}\n        ${90}  | ${'green'}\n        ${80}  | ${'green'}\n        ${60}  | ${'yellow'}\n        ${50}  | ${'yellow'}\n        ${40}  | ${'red'}\n        ${5}   | ${'red'}\n        ${20}  | ${'red'}\n        ${5}   | ${'red'}\n    `('should result in color \"$color\" with health $health', ({ health, color }) => {\n        expect(passwordStrengthColorExtractor(health)).toBe(color)\n    })\n})\n"
  },
  {
    "path": "test/SecretsDirectoryService.test.ts",
    "content": "import { Tree } from '../src/renderer/components/tree/TreeComponent'\nimport SecretsDirectoryService from '../src/renderer/explorer-app/side-navigation/SecretsDirectoryService'\n\ndescribe('SecretsDirectoryService', () => {\n    it('should transform a list of secret names into tree structure', () => {\n        const secretPaths = [\n            'codecentric/common/github/password',\n            'codecentric/common/github/username',\n            'codecentric/common/gitlab/password',\n            'codecentric/common/gitlab/username',\n            'codecentric/customers/some/notes'\n        ]\n        const tree: Tree = SecretsDirectoryService.secretPathsToTree(secretPaths, { name: 'someName', path: '' }, false)\n        expect(tree).toMatchSnapshot()\n    })\n\n    it('should automatically toggle all nodes', () => {\n        const secretPaths = [\n            'codecentric/some-secret',\n            'codecentric/common/something',\n            'codecentric/common/another-thing',\n            'codecentric/db/user',\n            'codecentric/db/password'\n        ]\n        const tree: Tree = SecretsDirectoryService.secretPathsToTree(secretPaths, { name: 'someName', path: '' }, true)\n        expect(tree).toMatchSnapshot()\n    })\n\n    it('should preserve previously toggled nodes when building a new tree', () => {\n        const secretPaths = [\n            'codecentric/some-secret',\n            'codecentric/common/something',\n            'codecentric/common/another-thing',\n            'codecentric/db/user',\n            'codecentric/db/password'\n        ]\n        const tree: Tree = SecretsDirectoryService.secretPathsToTree(\n            secretPaths,\n            {\n                name: '',\n                path: '',\n                children: [\n                    {\n                        name: 'codecentric',\n                        path: 'codecentric',\n                        toggled: true,\n                        children: [\n                            { name: 'some-secret', path: 'codecentric/some-secret' },\n                            { name: 'common', path: 'codecentric/common', toggled: true }\n                        ]\n                    }\n                ]\n            },\n            false\n        )\n        expect(tree).toMatchSnapshot()\n    })\n})\n"
  },
  {
    "path": "test/SecretsFilterService.test.ts",
    "content": "import SecretsFilterService from '../src/renderer/explorer-app/side-navigation/SecretsFilterService'\n\nconst secretNames = [\n    'codecentric/common/github/password',\n    'codecentric/common/github/username',\n    'codecentric/common/gitlab/password',\n    'codecentric/common/gitlab/username',\n    'codecentric/customers/some/notes',\n    'codecentric/some-project/cassandra/dev/pw',\n    'codecentric/some-project/cassandra/dev/user',\n    'codecentric/some-project/cassandra/prod/pw',\n    'codecentric/some-project/cassandra/prod/user'\n]\n\ndescribe('SecretsFilterService', () => {\n    it('should do a simple search on names', () => {\n        const results = SecretsFilterService.filterBySearch(secretNames, 'cassandra')\n        expect(results.length).toBe(4)\n    })\n\n    it('should do a search with two terms on names', () => {\n        const results = SecretsFilterService.filterBySearch(secretNames, 'cassandra user')\n        expect(results.length).toBe(2)\n\n        const moreResults = SecretsFilterService.filterBySearch(secretNames, 'cassandra user prod')\n        expect(moreResults.length).toBe(1)\n    })\n\n    it('should do a search with two terms on names without matches', () => {\n        const results = SecretsFilterService.filterBySearch(secretNames, 'cassandra user pp')\n        expect(results.length).toBe(0)\n    })\n})\n"
  },
  {
    "path": "test/__snapshots__/SecretsDirectoryService.test.ts.snap",
    "content": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`SecretsDirectoryService should automatically toggle all nodes 1`] = `\nObject {\n  \"children\": Array [\n    Object {\n      \"children\": Array [\n        Object {\n          \"children\": undefined,\n          \"name\": \"some-secret\",\n          \"path\": \"codecentric/some-secret\",\n          \"toggled\": true,\n        },\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": undefined,\n              \"name\": \"something\",\n              \"path\": \"codecentric/common/something\",\n              \"toggled\": true,\n            },\n            Object {\n              \"children\": undefined,\n              \"name\": \"another-thing\",\n              \"path\": \"codecentric/common/another-thing\",\n              \"toggled\": true,\n            },\n          ],\n          \"name\": \"common\",\n          \"path\": \"codecentric/common\",\n          \"toggled\": true,\n        },\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": undefined,\n              \"name\": \"user\",\n              \"path\": \"codecentric/db/user\",\n              \"toggled\": true,\n            },\n            Object {\n              \"children\": undefined,\n              \"name\": \"password\",\n              \"path\": \"codecentric/db/password\",\n              \"toggled\": true,\n            },\n          ],\n          \"name\": \"db\",\n          \"path\": \"codecentric/db\",\n          \"toggled\": true,\n        },\n      ],\n      \"name\": \"codecentric\",\n      \"path\": \"codecentric\",\n      \"toggled\": true,\n    },\n  ],\n  \"name\": \"Stores\",\n  \"path\": \"\",\n  \"toggled\": true,\n}\n`;\n\nexports[`SecretsDirectoryService should preserve previously toggled nodes when building a new tree 1`] = `\nObject {\n  \"children\": Array [\n    Object {\n      \"children\": Array [\n        Object {\n          \"children\": undefined,\n          \"name\": \"some-secret\",\n          \"path\": \"codecentric/some-secret\",\n          \"toggled\": false,\n        },\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": undefined,\n              \"name\": \"something\",\n              \"path\": \"codecentric/common/something\",\n              \"toggled\": false,\n            },\n            Object {\n              \"children\": undefined,\n              \"name\": \"another-thing\",\n              \"path\": \"codecentric/common/another-thing\",\n              \"toggled\": false,\n            },\n          ],\n          \"name\": \"common\",\n          \"path\": \"codecentric/common\",\n          \"toggled\": true,\n        },\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": undefined,\n              \"name\": \"user\",\n              \"path\": \"codecentric/db/user\",\n              \"toggled\": false,\n            },\n            Object {\n              \"children\": undefined,\n              \"name\": \"password\",\n              \"path\": \"codecentric/db/password\",\n              \"toggled\": false,\n            },\n          ],\n          \"name\": \"db\",\n          \"path\": \"codecentric/db\",\n          \"toggled\": false,\n        },\n      ],\n      \"name\": \"codecentric\",\n      \"path\": \"codecentric\",\n      \"toggled\": true,\n    },\n  ],\n  \"name\": \"Stores\",\n  \"path\": \"\",\n  \"toggled\": true,\n}\n`;\n\nexports[`SecretsDirectoryService should transform a list of secret names into tree structure 1`] = `\nObject {\n  \"children\": Array [\n    Object {\n      \"children\": Array [\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": Array [\n                Object {\n                  \"children\": undefined,\n                  \"name\": \"password\",\n                  \"path\": \"codecentric/common/github/password\",\n                  \"toggled\": false,\n                },\n                Object {\n                  \"children\": undefined,\n                  \"name\": \"username\",\n                  \"path\": \"codecentric/common/github/username\",\n                  \"toggled\": false,\n                },\n              ],\n              \"name\": \"github\",\n              \"path\": \"codecentric/common/github\",\n              \"toggled\": false,\n            },\n            Object {\n              \"children\": Array [\n                Object {\n                  \"children\": undefined,\n                  \"name\": \"password\",\n                  \"path\": \"codecentric/common/gitlab/password\",\n                  \"toggled\": false,\n                },\n                Object {\n                  \"children\": undefined,\n                  \"name\": \"username\",\n                  \"path\": \"codecentric/common/gitlab/username\",\n                  \"toggled\": false,\n                },\n              ],\n              \"name\": \"gitlab\",\n              \"path\": \"codecentric/common/gitlab\",\n              \"toggled\": false,\n            },\n          ],\n          \"name\": \"common\",\n          \"path\": \"codecentric/common\",\n          \"toggled\": false,\n        },\n        Object {\n          \"children\": Array [\n            Object {\n              \"children\": Array [\n                Object {\n                  \"children\": undefined,\n                  \"name\": \"notes\",\n                  \"path\": \"codecentric/customers/some/notes\",\n                  \"toggled\": false,\n                },\n              ],\n              \"name\": \"some\",\n              \"path\": \"codecentric/customers/some\",\n              \"toggled\": false,\n            },\n          ],\n          \"name\": \"customers\",\n          \"path\": \"codecentric/customers\",\n          \"toggled\": false,\n        },\n      ],\n      \"name\": \"codecentric\",\n      \"path\": \"codecentric\",\n      \"toggled\": true,\n    },\n  ],\n  \"name\": \"Stores\",\n  \"path\": \"\",\n  \"toggled\": true,\n}\n`;\n"
  },
  {
    "path": "test/deriveIconFromSecretName.test.ts",
    "content": "import { deriveIconFromSecretName } from '../src/renderer/secrets/deriveIconFromSecretName'\n\ndescribe('deriveIconFromSecretName', () => {\n    it('should derive \"comment\" icon on unclassifiable secret names', () => {\n        expect(deriveIconFromSecretName('blablabla')).toBe('comment')\n    })\n\n    describe('\"lock\" icon', () => {\n        const testCases = [\n            { secretName: 'some/random/password' },\n            { secretName: 'some/random/secret' },\n            { secretName: 'some/random/phraseapp-key' },\n            { secretName: 'some/random/passphrase' },\n            { secretName: 'some/random/certificate' }\n        ]\n\n        testCases.forEach(testCase => {\n            it(`should derive icon \"lock\" from secret name \"${testCase.secretName}\" which is indicating a password`, () => {\n                const result = deriveIconFromSecretName(testCase.secretName)\n                expect(result).toBe('lock')\n            })\n        })\n    })\n\n    describe('\"person\" icon', () => {\n        const testCases = [{ secretName: 'some/random/user' }, { secretName: 'some/random/name' }, { secretName: 'some/random/id' }]\n\n        testCases.forEach(testCase => {\n            it(`should derive icon \"person\" from secret name \"${testCase.secretName}\"`, () => {\n                const result = deriveIconFromSecretName(testCase.secretName)\n                expect(result).toBe('person')\n            })\n        })\n    })\n\n    describe('\"comment\" icon', () => {\n        const testCases = [\n            { secretName: 'some/random/something-without-pattern' },\n            { secretName: 'some/random/note' },\n            { secretName: 'some/random/comment' },\n            { secretName: 'some/random/misc' }\n        ]\n\n        testCases.forEach(testCase => {\n            it(`should derive icon \"comment\" from secret name \"${testCase.secretName}\"`, () => {\n                const result = deriveIconFromSecretName(testCase.secretName)\n                expect(result).toBe('comment')\n            })\n        })\n    })\n\n    describe('\"filter_center_focus\" icon', () => {\n        const testCases = [\n            { secretName: 'some/random/uri' },\n            { secretName: 'some/random/url' },\n            { secretName: 'some/random/link' },\n            { secretName: 'some/random/connection' }\n        ]\n\n        testCases.forEach(testCase => {\n            it(`should derive icon \"filter_center_focus\" from secret name \"${testCase.secretName}\"`, () => {\n                const result = deriveIconFromSecretName(testCase.secretName)\n                expect(result).toBe('filter_center_focus')\n            })\n        })\n    })\n})\n"
  },
  {
    "path": "test/mock/electron-mock.ts",
    "content": "import createIPCMock from 'electron-mock-ipc'\n\nconst mocked = createIPCMock()\nconst ipcMain = mocked.ipcMain\nconst ipcRenderer = mocked.ipcRenderer\n\nexport { ipcMain, ipcRenderer }\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"es5\",\n        \"module\": \"commonjs\",\n        \"lib\": [\n          \"dom\",\n          \"es2015\",\n          \"es2016\",\n          \"es2017\"\n      ],\n        \"allowJs\": true,\n        \"jsx\": \"react\",\n        \"sourceMap\": true,\n        \"strict\": true\n    }\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n    \"extends\": \"tslint:latest\",\n    \"jsRules\": {\n        \"quotemark\": [\n            true,\n            \"single\",\n            \"avoid-escape\"\n        ],\n        \"object-literal-sort-keys\": false,\n        \"arrow-parens\": false,\n        \"one-variable-per-declaration\": [\n            true,\n            \"ignore-for-loop\"\n        ],\n        \"semicolon\": [\n            true,\n            \"never\"\n        ],\n        \"trailing-comma\": [\n            true,\n            {\n                \"multiline\": \"never\",\n                \"singleline\": \"never\"\n            }\n        ],\n        \"object-literal-key-quotes\": [\n            true,\n            \"as-needed\"\n        ],\n        \"prefer-const\": true,\n        \"no-magic-numbers\": false,\n        \"only-arrow-functions\": [\n            true,\n            \"allow-declarations\",\n            \"allow-named-functions\"\n        ],\n        \"curly\": true,\n        \"no-console\": [\n            true,\n            \"log\",\n            \"error\"\n        ],\n        \"no-empty\": true,\n        \"no-invalid-this\": [\n            true,\n            \"check-function-in-method\"\n        ],\n        \"no-shadowed-variable\": true,\n        \"radix\": true,\n        \"switch-default\": true,\n        \"cyclomatic-complexity\": [\n            true,\n            10\n        ],\n        \"max-line-length\": [\n            true,\n            160\n        ]\n    },\n    \"rules\": {\n        \"no-implicit-dependencies\": false,\n        \"no-submodule-imports\": false,\n        \"quotemark\": [\n            true,\n            \"single\",\n            \"avoid-escape\"\n        ],\n        \"object-literal-sort-keys\": false,\n        \"arrow-parens\": false,\n        \"one-variable-per-declaration\": [\n            true,\n            \"ignore-for-loop\"\n        ],\n        \"semicolon\": [\n            true,\n            \"never\"\n        ],\n        \"interface-name\": [\n            true,\n            \"never-prefix\"\n        ],\n        \"trailing-comma\": [\n            true,\n            {\n                \"multiline\": \"never\",\n                \"singleline\": \"never\"\n            }\n        ],\n        \"object-literal-key-quotes\": [\n            true,\n            \"as-needed\"\n        ],\n        \"member-ordering\": [\n            true,\n            {\n                \"order\": \"fields-first\"\n            }\n        ],\n        \"ordered-imports\": false,\n        \"prefer-const\": true,\n        \"no-magic-numbers\": false,\n        \"only-arrow-functions\": [\n            true,\n            \"allow-declarations\",\n            \"allow-named-functions\"\n        ],\n        \"curly\": true,\n        \"no-console\": [\n            true,\n            \"log\"\n        ],\n        \"no-empty\": true,\n        \"no-empty-interface\": false,\n        \"no-invalid-this\": [\n            true,\n            \"check-function-in-method\"\n        ],\n        \"no-shadowed-variable\": true,\n        \"no-unused-expression\": false,\n        \"no-object-literal-type-assertion\": false,\n        \"radix\": true,\n        \"switch-default\": true,\n        \"cyclomatic-complexity\": [\n            true,\n            10\n        ],\n        \"max-line-length\": [\n            true,\n            160\n        ]\n    }\n}\n"
  },
  {
    "path": "webpack.base.config.js",
    "content": "'use strict'\n\nconst path = require('path')\n\nmodule.exports = {\n    output: {\n        path: path.resolve(__dirname, 'dist'),\n        filename: '[name].js'\n    },\n    node: {\n        __dirname: false,\n        __filename: false\n    },\n    resolve: {\n        extensions: ['.tsx', '.ts', '.js', '.json']\n    },\n    devtool: 'source-map',\n    plugins: [],\n    mode: 'development'\n}\n"
  },
  {
    "path": "webpack.main.config.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n\nconst baseConfig = require('./webpack.base.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    target: 'electron-main',\n    entry: {\n        main: './src/main/index.ts'\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.tsx?$/,\n                include: [path.resolve(__dirname, 'src', 'main'), path.resolve(__dirname, 'src', 'shared')],\n                loader: 'awesome-typescript-loader'\n            }\n        ]\n    },\n    plugins: [\n        new CopyWebpackPlugin([\n            {\n                from: 'src/main/assets',\n                to: 'assets'\n            }\n        ]),\n        new webpack.DefinePlugin({\n            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')\n        })\n    ]\n})\n"
  },
  {
    "path": "webpack.main.prod.config.js",
    "content": "const merge = require('webpack-merge')\n\nconst baseConfig = require('./webpack.main.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    plugins: [],\n    mode: 'production'\n})\n"
  },
  {
    "path": "webpack.renderer.explorer.config.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\n\nconst baseConfig = require('./webpack.base.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    output: {\n        path: path.resolve(__dirname, 'dist', 'explorer')\n    },\n    target: 'electron-renderer',\n    entry: {\n        app: './src/renderer/explorer-app.tsx'\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.tsx?$/,\n                include: [path.resolve(__dirname, 'src', 'renderer')],\n                loader: 'awesome-typescript-loader'\n            },\n            {\n                test: /\\.scss$/,\n                loaders: ['style-loader', 'css-loader', 'sass-loader']\n            },\n            {\n                test: /\\.css$/,\n                loaders: ['style-loader', 'css-loader']\n            },\n            {\n                test: /\\.(gif|png|jpe?g|svg)$/,\n                use: [\n                    'file-loader',\n                    {\n                        loader: 'image-webpack-loader',\n                        options: {\n                            bypassOnDebug: true\n                        }\n                    }\n                ]\n            },\n            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.\n            {\n                enforce: 'pre',\n                test: /\\.js$/,\n                loader: 'source-map-loader'\n            },\n            {\n                test: /\\.(woff(2)?|ttf|eot|svg)(\\?v=\\d+\\.\\d+\\.\\d+)?$/,\n                use: [\n                    {\n                        loader: 'file-loader',\n                        options: {\n                            name: '[name].[ext]',\n                            outputPath: 'fonts/'\n                        }\n                    }\n                ]\n            }\n        ]\n    },\n    plugins: [\n        new HtmlWebpackPlugin({\n            title: 'Gopass UI'\n        }),\n        new webpack.DefinePlugin({\n            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')\n        })\n    ]\n})\n"
  },
  {
    "path": "webpack.renderer.explorer.dev.config.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst spawn = require('child_process').spawn\n\nconst baseConfig = require('./webpack.renderer.explorer.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    entry: [\n        'react-hot-loader/patch',\n        './src/renderer/explorer-app.tsx'\n    ],\n    module: {\n        rules: [\n            {\n                test: /\\.tsx?$/,\n                include: [ path.resolve(__dirname, 'src', 'renderer') ],\n                loaders: [ 'react-hot-loader/webpack', 'awesome-typescript-loader' ]\n            }\n        ]\n    },\n    plugins: [\n        new webpack.NamedModulesPlugin(),\n        new webpack.HotModuleReplacementPlugin()\n    ],\n    devServer: {\n        port: 2003,\n        compress: true,\n        noInfo: true,\n        stats: 'errors-only',\n        inline: true,\n        hot: true,\n        headers: { 'Access-Control-Allow-Origin': '*' },\n        historyApiFallback: {\n            verbose: true,\n            disableDotRule: false\n        },\n        before() {\n            if (process.env.START_HOT) {\n                console.log('Starting main process');\n                spawn('npm', ['run', 'start-main-dev'], {\n                    shell: true,\n                    env: process.env,\n                    stdio: 'inherit'\n                })\n                    .on('close', code => process.exit(code))\n                    .on('error', spawnError => console.error(spawnError));\n            }\n        }\n    }\n})\n"
  },
  {
    "path": "webpack.renderer.explorer.prod.config.js",
    "content": "const merge = require('webpack-merge')\n\nconst baseConfig = require('./webpack.renderer.explorer.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    plugins: [],\n    mode: 'production'\n})\n"
  },
  {
    "path": "webpack.renderer.search.config.js",
    "content": "const merge = require('webpack-merge')\nconst path = require('path')\n\nconst baseConfig = require('./webpack.renderer.explorer.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    output: {\n        path: path.resolve(__dirname, 'dist', 'search'),\n    },\n    target: 'electron-renderer',\n    entry: {\n        app: './src/renderer/search-app.tsx'\n    }\n})\n"
  },
  {
    "path": "webpack.renderer.search.dev.config.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst spawn = require('child_process').spawn\n\nconst baseConfig = require('./webpack.renderer.search.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    entry: [\n        'react-hot-loader/patch',\n        './src/renderer/search-app.tsx'\n    ],\n    module: {\n        rules: [\n            {\n                test: /\\.tsx?$/,\n                include: [ path.resolve(__dirname, 'src', 'renderer') ],\n                loaders: [ 'react-hot-loader/webpack', 'awesome-typescript-loader' ]\n            }\n        ]\n    },\n    plugins: [\n        new webpack.NamedModulesPlugin(),\n        new webpack.HotModuleReplacementPlugin()\n    ],\n    devServer: {\n        port: 2004,\n        compress: true,\n        noInfo: true,\n        stats: 'errors-only',\n        inline: true,\n        hot: true,\n        headers: { 'Access-Control-Allow-Origin': '*' },\n        historyApiFallback: {\n            verbose: true,\n            disableDotRule: false\n        }\n    }\n})\n"
  },
  {
    "path": "webpack.renderer.search.prod.config.js",
    "content": "const merge = require('webpack-merge')\n\nconst baseConfig = require('./webpack.renderer.search.config')\n\nmodule.exports = merge.smart(baseConfig, {\n    plugins: [],\n    mode: 'production'\n})\n"
  }
]