├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── launch.sh ├── lib ├── main.js ├── media │ ├── dock.png │ ├── gmail.png │ └── screenshot.png ├── menu.js ├── preload.js └── ui │ ├── gmail.css │ └── gmail.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paulo Tanaka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  Gmail 2 | 3 | > An Unofficial Gmail native web client built with Electron JS. 4 | 5 |
6 | [![](lib/media/screenshot.png)](https://github.com/paulot/gmail/releases/latest) 7 | [![](lib/media/dock.png)](https://github.com/paulot/gmail/releases/latest) 8 | 9 | ## Install 10 | Check the current list of [releases](https://github.com/paulot/gmail/releases/latest) for prebuilt binaries. 11 | 12 | #### Mac OS 13 | Simply drag the .app file located in the archive to your dock 14 | 15 | #### Windows/Linux 16 | Still working on a binary. There are still a few issues with the menu that need to be sorted out in Linux. 17 | 18 | #### Running from source 19 | - Clone the repo: `git clone https://github.com/paulot/gmail.git` 20 | - Install dependencies: `npm install` 21 | - Run: `npm start` 22 | 23 | ## Development 24 | Built with [Electron JS](http://electron.atom.io). 25 | 26 | #### Commands 27 | - Init: `$ npm install` 28 | - Run: `$ npm start` 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | require('./lib/main'); 3 | -------------------------------------------------------------------------------- /launch.sh: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/electron . 2 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | import app from 'app'; 2 | import BrowserWindow from 'browser-window'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import os from 'os'; 6 | import electron from 'electron'; 7 | import Promise from 'bluebird'; 8 | import { menu as appMenu } from './menu'; 9 | 10 | let mainWindow = null; 11 | 12 | const gmailURL = 'http://www.gmail.com'; 13 | const gmailLogoutRe = 'https://mail.google.com/mail/logout'; 14 | const gmailAddAccountRe = 'https://accounts.google.com/AddSession'; 15 | const oktaRe = 'https://.*.okta.com/'; 16 | const gmailDomainRe = 'https://mail.google.com/'; 17 | const editInNewTabRe = 'https://mail.google.com/mail/.*#cmid%253D[0-9]+'; 18 | 19 | // Set os specific stuff 20 | electron.ipcMain.on('update-dock', function(event, arg) { 21 | if (os.platform() === 'darwin') { 22 | if (arg > 0) { 23 | // Hide dock badge when unread mail count is 0 24 | app.dock.setBadge(arg.toString()); 25 | } else { 26 | app.dock.setBadge(''); 27 | } 28 | } 29 | }); 30 | 31 | 32 | function createWindow() { 33 | if (mainWindow) return mainWindow; 34 | mainWindow = new BrowserWindow({ 35 | title: 'Gmail', 36 | icon: path.join(__dirname, 'media', 'gmail.png'), 37 | width: 800, 38 | height: 600, 39 | minWidth: 400, 40 | minHeight: 200, 41 | titleBarStyle: 'hidden', 42 | webPreferences: { 43 | nodeIntegration: false, 44 | preload: path.join(__dirname, 'preload.js'), 45 | webSecurity: false, 46 | plugins: true 47 | } 48 | }); 49 | 50 | mainWindow.loadURL(gmailURL); 51 | mainWindow.maximize(); 52 | mainWindow.on('close', app.quit); 53 | 54 | return mainWindow; 55 | } 56 | 57 | function gotoURL(url) { 58 | return new Promise((resolve) => { 59 | mainWindow.webContents.on('did-finish-load', resolve); 60 | mainWindow.webContents.loadURL(url); 61 | }); 62 | } 63 | 64 | 65 | app.on('ready', () => { 66 | electron.Menu.setApplicationMenu(appMenu); 67 | 68 | createWindow(); 69 | let page = mainWindow.webContents; 70 | 71 | page.on('dom-ready', () => { 72 | page.insertCSS(fs.readFileSync(path.join(__dirname, 'ui', 'gmail.css'), 'utf8')); 73 | }); 74 | 75 | // Open links in default browser 76 | page.on('new-window', function(e, url) { 77 | if (url.match(gmailLogoutRe)) { 78 | e.preventDefault(); 79 | gotoURL(url).then(() => { gotoURL(gmailURL) }); 80 | } else if (url.match(editInNewTabRe)) { 81 | e.preventDefault(); 82 | page.send('start-compose'); 83 | } else if (url.match(gmailDomainRe) || 84 | url.match(gmailAddAccountRe) || 85 | url.match(oktaRe)) { 86 | e.preventDefault(); 87 | page.loadURL(url); 88 | } else { 89 | e.preventDefault(); 90 | require('shell').openExternal(url); 91 | } 92 | }); 93 | 94 | // mainWindow.webContents.openDevTools(); 95 | }); 96 | -------------------------------------------------------------------------------- /lib/media/dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulot/gmail/0a1b8d451d8f330060ea10d09b9481e21dd4226a/lib/media/dock.png -------------------------------------------------------------------------------- /lib/media/gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulot/gmail/0a1b8d451d8f330060ea10d09b9481e21dd4226a/lib/media/gmail.png -------------------------------------------------------------------------------- /lib/media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulot/gmail/0a1b8d451d8f330060ea10d09b9481e21dd4226a/lib/media/screenshot.png -------------------------------------------------------------------------------- /lib/menu.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import app from 'app'; 3 | import os from 'os'; 4 | 5 | const appName = app.getName(); 6 | 7 | const darwinTpl = [ 8 | { 9 | label: appName, 10 | submenu: [ 11 | { 12 | label: `About ${appName}`, 13 | role: 'about' 14 | }, 15 | { type: 'separator' }, 16 | { 17 | label: 'Preferences...', 18 | accelerator: 'Cmd+,', 19 | click(item, focusedWindow) { 20 | if (focusedWindow) 21 | focusedWindow.webContents.send('navigate', 'settings/general'); 22 | } 23 | }, 24 | { 25 | label: 'Services', 26 | role: 'services', 27 | submenu: [] 28 | }, 29 | { type: 'separator' }, 30 | { 31 | label: 'Log Out', 32 | click(item, focusedWindow) { 33 | if (focusedWindow) 34 | focusedWindow.webContents.send('logout'); 35 | } 36 | }, 37 | { type: 'separator' }, 38 | { 39 | label: `Hide ${appName}`, 40 | accelerator: 'Cmd+H', 41 | role: 'hide' 42 | }, 43 | { 44 | label: 'Hide Others', 45 | accelerator: 'Cmd+Shift+H', 46 | role: 'hideothers' 47 | }, 48 | { 49 | label: 'Show All', 50 | role: 'unhide' 51 | }, 52 | { type: 'separator' }, 53 | { 54 | label: `Quit ${appName}`, 55 | accelerator: 'Cmd+Q', 56 | click() { 57 | app.quit(); 58 | } 59 | } 60 | ] 61 | }, 62 | { 63 | label: 'Mail', 64 | submenu: [ 65 | { 66 | label: 'Compose', 67 | accelerator: 'CmdOrCtrl+N', 68 | click(item, focusedWindow) { 69 | if (focusedWindow) 70 | focusedWindow.webContents.send('start-compose'); 71 | } 72 | }, 73 | { type: 'separator' }, 74 | { 75 | label: 'Go To...', 76 | submenu: [ 77 | { 78 | label: 'Inbox', 79 | accelerator: 'CmdOrCtrl+I', 80 | click(item, focusedWindow) { 81 | if (focusedWindow) 82 | focusedWindow.webContents.send('navigate', 'inbox'); 83 | } 84 | }, { 85 | label: 'Sent', 86 | accelerator: 'Shift+CmdOrCtrl+S', 87 | click(item, focusedWindow) { 88 | if (focusedWindow) 89 | focusedWindow.webContents.send('navigate', 'sent'); 90 | } 91 | }, { 92 | label: 'Starred', 93 | accelerator: 'Shift+CmdOrCtrl+R', 94 | click(item, focusedWindow) { 95 | if (focusedWindow) 96 | focusedWindow.webContents.send('navigate', 'starred'); 97 | } 98 | }, { 99 | label: 'Drafts', 100 | accelerator: 'Shift+CmdOrCtrl+D', 101 | click(item, focusedWindow) { 102 | if (focusedWindow) 103 | focusedWindow.webContents.send('navigate', 'drafts'); 104 | } 105 | }, { 106 | label: 'Important', 107 | accelerator: 'Shift+CmdOrCtrl+I', 108 | click(item, focusedWindow) { 109 | if (focusedWindow) 110 | focusedWindow.webContents.send('navigate', 'imp'); 111 | } 112 | }, { 113 | label: 'Chats', 114 | accelerator: 'Shift+CmdOrCtrl+C', 115 | click(item, focusedWindow) { 116 | if (focusedWindow) 117 | focusedWindow.webContents.send('navigate', 'chats'); 118 | } 119 | }, { 120 | label: 'All', 121 | accelerator: 'Shift+CmdOrCtrl+A', 122 | click(item, focusedWindow) { 123 | if (focusedWindow) 124 | focusedWindow.webContents.send('navigate', 'all'); 125 | } 126 | }, { 127 | label: 'Spam', 128 | accelerator: 'Shift+CmdOrCtrl+P', 129 | click(item, focusedWindow) { 130 | if (focusedWindow) 131 | focusedWindow.webContents.send('navigate', 'spam'); 132 | } 133 | }, { 134 | label: 'Trash', 135 | accelerator: 'Shift+CmdOrCtrl+T', 136 | click(item, focusedWindow) { 137 | if (focusedWindow) 138 | focusedWindow.webContents.send('navigate', 'trash'); 139 | } 140 | } 141 | ] 142 | } 143 | ] 144 | },{ 145 | label: 'Edit', 146 | submenu: [ 147 | { 148 | label: 'Undo', 149 | accelerator: 'CmdOrCtrl+Z', 150 | role: 'undo' 151 | }, 152 | { 153 | label: 'Redo', 154 | accelerator: 'Shift+CmdOrCtrl+Z', 155 | role: 'redo' 156 | }, 157 | { type: 'separator' }, 158 | { 159 | label: 'Cut', 160 | accelerator: 'CmdOrCtrl+X', 161 | role: 'cut' 162 | }, 163 | { 164 | label: 'Copy', 165 | accelerator: 'CmdOrCtrl+C', 166 | role: 'copy' 167 | }, 168 | { 169 | label: 'Paste', 170 | accelerator: 'CmdOrCtrl+V', 171 | role: 'paste' 172 | }, 173 | { 174 | label: 'Select All', 175 | accelerator: 'CmdOrCtrl+A', 176 | role: 'selectall' 177 | } 178 | ] 179 | },{ 180 | label: 'Window', 181 | role: 'window', 182 | submenu: [ 183 | { 184 | label: 'Minimize', 185 | accelerator: 'CmdOrCtrl+M', 186 | role: 'minimize' 187 | }, 188 | { 189 | label: 'Close', 190 | accelerator: 'CmdOrCtrl+W', 191 | role: 'close' 192 | }, 193 | { type: 'separator' }, 194 | { 195 | label: 'Go Back', 196 | accelerator: 'CmdOrCtrl+Backspace', 197 | click(item, focusedWindow) { 198 | if (focusedWindow && focusedWindow.webContents.canGoBack()) 199 | focusedWindow.webContents.goBack(); 200 | } 201 | }, { 202 | label: 'Go Forward', 203 | accelerator: 'Cmd+Ctrl+F', 204 | click(item, focusedWindow) { 205 | if (focusedWindow && focusedWindow.webContents.canGoForward()) 206 | focusedWindow.webContents.goForward(); 207 | } 208 | }, { 209 | label: 'Reload', 210 | accelerator: 'CmdOrCtrl+R', 211 | click(item, focusedWindow) { 212 | if (focusedWindow) 213 | focusedWindow.webContents.reload(); 214 | } 215 | }, 216 | { type: 'separator' }, 217 | { 218 | label: 'Bring All to Front', 219 | role: 'front' 220 | }, 221 | { 222 | label: 'Toggle Full Screen', 223 | accelerator: 'Ctrl+Cmd+F', 224 | click(item, focusedWindow) { 225 | if (focusedWindow) 226 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); 227 | } 228 | } 229 | ] 230 | },{ 231 | label: 'Settings', 232 | submenu: [ 233 | { 234 | label: 'General', 235 | click(item, focusedWindow) { 236 | if (focusedWindow) 237 | focusedWindow.webContents.send('navigate', 'settings/general'); 238 | } 239 | }, { 240 | label: 'Labels', 241 | click(item, focusedWindow) { 242 | if (focusedWindow) 243 | focusedWindow.webContents.send('navigate', 'settings/labels'); 244 | } 245 | }, { 246 | label: 'Inbox', 247 | click(item, focusedWindow) { 248 | if (focusedWindow) 249 | focusedWindow.webContents.send('navigate', 'settings/inbox'); 250 | } 251 | }, { 252 | label: 'Accounts and Import', 253 | click(item, focusedWindow) { 254 | if (focusedWindow) 255 | focusedWindow.webContents.send('navigate', 'settings/accounts'); 256 | } 257 | }, { 258 | label: 'Filters and Blocked Addresses', 259 | click(item, focusedWindow) { 260 | if (focusedWindow) 261 | focusedWindow.webContents.send('navigate', 'settings/filters'); 262 | } 263 | }, { 264 | label: 'Forwarding and POP/IMAP', 265 | click(item, focusedWindow) { 266 | if (focusedWindow) 267 | focusedWindow.webContents.send('navigate', 'settings/fwdandpop'); 268 | } 269 | }, { 270 | label: 'Chat', 271 | click(item, focusedWindow) { 272 | if (focusedWindow) 273 | focusedWindow.webContents.send('navigate', 'settings/chat'); 274 | } 275 | }, { 276 | label: 'Labs', 277 | click(item, focusedWindow) { 278 | if (focusedWindow) 279 | focusedWindow.webContents.send('navigate', 'settings/labs'); 280 | } 281 | }, { 282 | label: 'Offline', 283 | click(item, focusedWindow) { 284 | if (focusedWindow) 285 | focusedWindow.webContents.send('navigate', 'settings/offline'); 286 | } 287 | }, { 288 | label: 'Themes', 289 | click(item, focusedWindow) { 290 | if (focusedWindow) 291 | focusedWindow.webContents.send('navigate', 'settings/oldthemes'); 292 | } 293 | } 294 | ] 295 | }, { 296 | label: 'Help', 297 | role: 'help' 298 | } 299 | ]; 300 | 301 | const helpSubmenu = [ 302 | { 303 | label: `${appName}'s Project Website...`, 304 | click() { 305 | electron.shell.openExternal('https://github.com/paulot/gmail'); 306 | } 307 | }, 308 | { 309 | label: 'Report an Issue...', 310 | click() { 311 | const body = ` 312 | **Please succinctly describe your issue and steps to reproduce it.** 313 | - 314 | ${app.getName()} ${app.getVersion()} 315 | ${process.platform} ${process.arch} ${os.release()}`; 316 | 317 | electron.shell.openExternal(`https://github.com/paulot/gmail/issues/new?body=${encodeURIComponent(body)}`); 318 | } 319 | } 320 | ]; 321 | 322 | darwinTpl[darwinTpl.length - 1].submenu = helpSubmenu; 323 | 324 | export let menu = electron.Menu.buildFromTemplate(darwinTpl); 325 | -------------------------------------------------------------------------------- /lib/preload.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | var GmailApi = require('node-gmail'); 3 | var jquery = require('jquery'); 4 | var page = require('./ui/gmail.js'); 5 | var ipc = require('electron').ipcRenderer 6 | 7 | window.j = jquery; 8 | window.page = new page(); 9 | window.Gmail = GmailApi(jquery); 10 | 11 | function updateDock() { 12 | ipc.send('update-dock', window.Gmail.get.unread_inbox_emails()); 13 | } 14 | 15 | function setDockUpdaters() { 16 | var updateEvents = ['new_email', 'refresh', 'unread', 'read', 17 | 'delete', 'move_to_inbox', 'move_to_label']; 18 | for (var i = 0; i < updateEvents.length; i++) { 19 | window.Gmail.observe.on(updateEvents[i], updateDock); 20 | } 21 | } 22 | 23 | ipc.on('start-compose', window.Gmail.compose.start_compose.bind(window.page)); 24 | ipc.on('logout', window.page.logout.bind(window.page)); 25 | ipc.on('navigate', (event, place) => { 26 | window.page.navigateTo(place); 27 | }); 28 | 29 | updateDock(); 30 | setDockUpdaters(); 31 | window.page.adjustProfilePicture(); 32 | window.page.adjustLogoutButton(); 33 | window.page.applyHangoutsCss(); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/ui/gmail.css: -------------------------------------------------------------------------------- 1 | /* side menu */ 2 | .nH.oy8Mbf.nn.aeN { 3 | /* display: none; */ 4 | } 5 | 6 | /* keyboard thingy */ 7 | .aBS.J-J5-Ji { 8 | display: none; 9 | } 10 | 11 | /* other apps button */ 12 | #gbwa.gb_ca.gb_Rb.gb_R { 13 | /* display: none; */ 14 | } 15 | 16 | /* notification thingy */ 17 | .gb_Zb.gb_Rb.gb_R.gb_0b { 18 | display: none; 19 | } 20 | 21 | /* google hangouts/phone */ 22 | .akc.aZ6 { 23 | /* display: none; */ 24 | } 25 | 26 | /* google hangouts/phone slider */ 27 | .aeO { 28 | /* display: none; */ 29 | } 30 | 31 | /* google hangouts popout */ 32 | .o8qlBb.PJ .gGnOIc.x2.qp.DI.QmCEdd { 33 | display: none; 34 | } 35 | 36 | /* google/company logo */ 37 | .gb_tb a.gb_qc.gb_vb { 38 | /* display: none; */ 39 | margin-top: 10px; 40 | } 41 | 42 | /* window with emails */ 43 | .nH.nn { 44 | /* width: 100%; */ 45 | } 46 | 47 | /* Signin page stuff */ 48 | 49 | .google-footer-bar { 50 | display: none; 51 | } 52 | 53 | .tagline { 54 | display: none; 55 | } 56 | 57 | .main .one-google .logo-strip { 58 | display: none; 59 | } 60 | 61 | .banner h1 { 62 | display: none; 63 | } 64 | 65 | .main.content.clearfix { 66 | padding-bottom: 0px; 67 | } 68 | -------------------------------------------------------------------------------- /lib/ui/gmail.js: -------------------------------------------------------------------------------- 1 | var jQuery = require('jquery'); 2 | 3 | function Gmail() { 4 | this.gmailRootUrl = 'https://mail.google.com/mail/u/[0-9]+/'; 5 | 6 | this.sidebar = jQuery('.nH').find('.no').find('.nH.oy8Mbf.nn.aeN'); 7 | this.emailPane = jQuery(jQuery('.nH').find('.no').find('.nH.nn')[3]); 8 | this.profileView = jQuery('.gb_Pb.gb_le.gb_R'); 9 | this.logoutButton = jQuery('#gb_71.gb_Ba.gb_vd.gb_Cd.gb_9a'); 10 | 11 | this.settings = jQuery('.J-M.asi.aYO.jQjAxd').find('div:contains("Settings")'); 12 | } 13 | 14 | Gmail.prototype.adjustProfilePicture = function() { 15 | this.profileView.css('min-width', '20px'); 16 | }; 17 | 18 | Gmail.prototype.adjustLogoutButton = function() { 19 | this.logoutButton.attr('target', '_blank'); 20 | }; 21 | 22 | Gmail.prototype.logout = function() { 23 | this.logoutButton[0].click(); 24 | }; 25 | 26 | Gmail.prototype.navigateTo = function(place) { 27 | var root = window.location.href.match(this.gmailRootUrl); 28 | if (root) { 29 | root = root[0]; 30 | window.location.href = root + '#' + place; 31 | } 32 | }; 33 | 34 | module.exports = Gmail; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmail-app", 3 | "productName": "Gmail", 4 | "version": "0.0.1", 5 | "description": "A native gmail client.", 6 | "main": "index.js", 7 | "bin": { "gmail": "./launch.sh" }, 8 | "scripts": { 9 | "start": "./launch.sh", 10 | "build": "electron-packager . Gmail --platform=darwin --arch=x64 --version=0.35.1 --icon=lib/media/gmail.png", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/paulot/gmail.git" 16 | }, 17 | "keywords": [ 18 | "gmail", 19 | "mail", 20 | "app" 21 | ], 22 | "author": "Paulo Tanaka", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/paulot/gmail/issues" 26 | }, 27 | "homepage": "https://github.com/paulot/gmail#readme", 28 | "dependencies": { 29 | "bluebird": "^3.0.5", 30 | "jquery": "^2.1.4", 31 | "node-gmail": "^1.0.0" 32 | }, 33 | "devDependencies": { 34 | "babel-preset-es2015": "^6.1.18", 35 | "babel-core": "^6.2.1", 36 | "electron-prebuilt": "^0.35.1" 37 | } 38 | } 39 | --------------------------------------------------------------------------------