├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── THANKS.md ├── app ├── dist │ └── .gitkeep ├── electron.js ├── main.ejs ├── main │ ├── menu.js │ ├── repoUtils.js │ ├── rpc.js │ └── session.js ├── package.json └── src │ ├── App.vue │ ├── components │ ├── AboutView.vue │ ├── AppView │ │ ├── CustomCommandsInput.vue │ │ ├── CustomCommandsList.vue │ │ ├── HeaderBar.vue │ │ └── Settings.vue │ ├── HelpView.vue │ ├── RepoListView.vue │ ├── RepoListView │ │ ├── KnownRepos.vue │ │ └── OpenRepoButton.vue │ ├── RepoView.vue │ ├── RepoView │ │ ├── Command.vue │ │ ├── CommandOutput.vue │ │ └── Repo.vue │ └── UpdateAvailableView.vue │ ├── defaults.js │ ├── directives │ ├── KeyTracker.vue │ ├── OpenExternal.vue │ └── StayDown.vue │ ├── fonts │ └── lato │ │ ├── FONTLOG.txt │ │ ├── Lato-Black.woff2 │ │ ├── Lato-BlackItalic.woff2 │ │ ├── Lato-Bold.woff2 │ │ ├── Lato-BoldItalic.woff2 │ │ ├── Lato-Hairline.woff2 │ │ ├── Lato-HairlineItalic.woff2 │ │ ├── Lato-Italic.woff2 │ │ ├── Lato-Light.woff2 │ │ ├── Lato-LightItalic.woff2 │ │ ├── Lato-Regular.woff2 │ │ └── OFL.txt │ ├── main.js │ ├── modules │ ├── DomUtils.js │ ├── Hterm.js │ ├── Rpc.js │ └── WindowKeyManager.js │ ├── routes.js │ ├── styles │ ├── _utils.scss │ ├── animations │ │ └── _move-down.scss │ ├── components │ │ ├── _drag-handle.scss │ │ └── _logo.scss │ ├── fonts │ │ └── _lato.scss │ ├── objects │ │ ├── _buttons.scss │ │ ├── _checkbox.scss │ │ ├── _code.scss │ │ ├── _headlines.scss │ │ ├── _icon.scss │ │ ├── _input.scss │ │ ├── _lists.scss │ │ ├── _paragraphs.scss │ │ └── _small.scss │ └── transitions │ │ ├── _slide-down--slide-up.scss │ │ ├── _slide-left--slide-right.scss │ │ ├── _slide-right--slide-left.scss │ │ └── _slide-up--slide-down.scss │ └── vuex │ ├── actions.js │ ├── getters.js │ ├── modules │ ├── app.js │ ├── defaults.js │ ├── index.js │ ├── repos.js │ └── session.js │ └── store.js ├── build ├── background.png ├── background@2x.png └── icon.icns ├── config.js ├── devtools ├── backend.js ├── devtools.html ├── devtools.js └── hook.js ├── dist └── .gitkeep ├── media ├── logo.jpg └── preview.jpg ├── package.json ├── tasks ├── release.js ├── runner.js ├── vue │ ├── route.js │ ├── route.template.txt │ └── routes.template.txt └── vuex │ ├── module.js │ └── module.template.txt └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For all files in this project: 2 | # - 2-space soft tabs 3 | # - All files end with a line-break 4 | # - Trim trailing whitespace 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | app/node_modules/** 2 | app/dist/** 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'commonjs': true, 5 | 'es6': true, 6 | 'node': true 7 | }, 8 | plugins: [ 9 | 'html' 10 | ], 11 | 'extends': 'eslint:recommended', 12 | 'parserOptions': { 13 | 'sourceType': 'module' 14 | }, 15 | 'rules': { 16 | 'array-bracket-spacing' : [ 17 | 'error', 18 | 'always' 19 | ], 20 | 'key-spacing' : [ 21 | 2, 22 | { 23 | 'singleLine' : { 24 | 'beforeColon' : true, 25 | 'afterColon' : true 26 | }, 27 | 'multiLine': { 28 | 'beforeColon' : true, 29 | 'afterColon' : true, 30 | 'align' : 'colon' 31 | } 32 | } 33 | ], 34 | 'space-in-parens': [ 35 | 'error', 36 | 'always' 37 | ], 38 | 'indent': [ 39 | 'error', 40 | 2 41 | ], 42 | 'linebreak-style': [ 43 | 'error', 44 | 'unix' 45 | ], 46 | 'quotes': [ 47 | 'error', 48 | 'single' 49 | ], 50 | 'semi': [ 51 | 'error', 52 | 'always' 53 | ], 54 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 55 | 'no-console' : process.env.NODE_ENV === 'production' ? 2 : 0 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | app/dist/* 3 | dist/* 4 | node_modules 5 | npm-debug.log 6 | npm-debug.log.* 7 | thumbs.db 8 | !.gitkeep 9 | drafts.sketch -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: npm test 5 | sudo: required 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Forrest 2 | 3 | Forrest is a community-driven project. As such, we welcome and encourage all sorts of 4 | contributions. They include, but are not limited to: 5 | 6 | - Constructive feedback 7 | - Questions about usage 8 | - Documentation changes 9 | - Feature requests 10 | - [Pull requests](#filing-pull-requests) 11 | 12 | We strongly suggest that before filing an issue, you search through the existing issues to see 13 | if it has already been filed by someone else. 14 | 15 | ## Contribution suggestions 16 | 17 | We use the label [`help wanted`](https://github.com/stefanjudis/forrest/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) in the issue tracker to denote fairly-well-scoped-out bugs or feature requests that the community can pick up and work on. If any of those labeled issues do not have enough information, please feel free to ask constructive questions. (This applies to any open issue.) 18 | 19 | ## Filing Pull Requests 20 | 21 | Here are some things to keep in mind as you file pull requests to fix bugs, add new features, etc.: 22 | 23 | * Travis CI is used to make sure that the project builds packages as expected on the supported 24 | platforms, using supported Node.js versions. 25 | * Please make sure your commits are rebased onto the latest commit in the master branch, and that 26 | you limit/squash the number of commits created to a "feature"-level. For instance: 27 | 28 | bad: 29 | 30 | ``` 31 | commit 1: add foo option 32 | commit 2: standardize code 33 | commit 3: add test 34 | commit 4: add docs 35 | commit 5: add bar option 36 | commit 6: add test + docs 37 | ``` 38 | 39 | good: 40 | 41 | ``` 42 | commit 1: add foo option 43 | commit 2: add bar option 44 | ``` 45 | 46 | ## For Collaborators 47 | 48 | Make sure to get a `:thumbsup:`, `+1` or `LGTM` from another collaborator before merging a PR. 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stefan Judis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![image](./media/logo.jpg) 4 | 5 | [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/uses-html.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/uses-css.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/uses-js.svg)](http://forthebadge.com) 6 | 7 | # Forrest 8 | 9 | > "Run Forrest, run!!!" 10 | 11 | ![image](./media/preview.jpg) 12 | 13 | ## About 14 | 15 | Forrest is an npm desktop client to deal with daily work flows. It lets you control common npm commands and all custom scripts defined in the `package.json`. 16 | 17 | It's still in early stages, but you can download the latest version at the [releases section](https://github.com/stefanjudis/forrest/releases). Feedback is more than welcome and contributions would be even better. :) 18 | 19 | ### Forrest is an [OPEN Open Source Project](http://openopensource.org/) 20 | 21 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 22 | 23 | See [CONTRIBUTING.md](./CONTRIBUTING.md) and [openopensource.org](http://openopensource.org/) for more details. 24 | 25 | ## Thanks 26 | 27 | - [Evan You](https://github.com/yyx990803) | *for creation and maintenance of vue.js* 28 | - [Greg Holguin](https://github.com/SimulatedGREG) | *for creation of the electron vue boilerplate* 29 | - [Sindre Sorhus](https://github.com/sindresorhus) | *for all this great work and especially for solving the issues around environments and the holy PATH* 30 | - [Michael Kühnel](https://github.com/mischah) | *for giving Forrest its name* 31 | - Calvin Goodman | *for creation of the "Home" icon from the Noun Project* 32 | - Michal Beno | *for creation of the "Settings" icon from the Noun Project* 33 | 34 | Additionally Forrest relies on the work of a lot of open source maintainers. So I want to thank all these [people](./THANKS.md) for their great work, too. 35 | -------------------------------------------------------------------------------- /THANKS.md: -------------------------------------------------------------------------------- 1 | # Credits for forrest 2 | ## forrest relies on the work of 462 people: 3 | 4 | - **Sindre Sorhus** *sindresorhus@gmail.com* (119 packages) 5 | - **amasad** *amjad.masad@gmail.com* (72 packages) 6 | - **Sebastian McKenzie** *sebmck@gmail.com* (72 packages) 7 | - **Jesse McCarthy** *npm-public@jessemccarthy.net* (71 packages) 8 | - **hzoo** *hi@henryzoo.com* (71 packages) 9 | - **loganfsmyth** *loganfsmyth@gmail.com* (71 packages) 10 | - **thejameskyle** *me@thejameskyle.com* (69 packages) 11 | - **Isaac Z. Schlueter** *isaacs@npmjs.com* (57 packages) 12 | - **John-David Dalton** *john.david.dalton@gmail.com* (44 packages) 13 | - **Mathias Bynens** *mathias@qiwi.be* (43 packages) 14 | - **Jon Schlinkert** *github@sellside.com* (38 packages) 15 | - **Blaine Bublitz** *blaine@iceddev.com* (38 packages) 16 | - **Douglas Wilson** *doug@somethingdoug.com* (37 packages) 17 | - **Ben Briggs** *beneb.info@gmail.com* (29 packages) 18 | - **Rebecca Turner** *me@re-becca.org* (29 packages) 19 | - **Forrest L Norvell** *forrest@npmjs.com* (28 packages) 20 | - **James Halliday** *mail@substack.net* (27 packages) 21 | - **Tobias Koppers** *tobias.koppers@googlemail.com* (23 packages) 22 | - **Kat Marchaán** *kzm@sykosomatic.org* (23 packages) 23 | - **Blake Embrey** *hello@blakeembrey.com* (22 packages) 24 | - **TJ Holowaychuk** *tj@vision-media.ca* (21 packages) 25 | - **doowb** *brian.woodward@gmail.com* (21 packages) 26 | - **Tobias Koppers @sokra** (21 packages) 27 | - **Bogdan Chadkin** *trysound@yandex.ru* (20 packages) 28 | - **Jonathan Ong** *jonathanrichardong@gmail.com* (18 packages) 29 | - **Kit Cambridge** *github@kitcambridge.be* (17 packages) 30 | - **Benjamin Tan** *demoneaux@gmail.com* (16 packages) 31 | - **Dominic Tarr** *dominic.tarr@gmail.com* (14 packages) 32 | - **Elan Shanker** *elan.shanker+npm@gmail.com* (13 packages) 33 | - **Benjamin Coe** *ben@npmjs.com* (11 packages) 34 | - **Roman Shtylman** *shtylman@gmail.com* (11 packages) 35 | - **Nathan Rajlich** *nathan@tootallnate.net* (10 packages) 36 | - **Felix Böhm** *me@feedic.com* (10 packages) 37 | - **Mathias Buus** *mathiasbuus@gmail.com* (9 packages) 38 | - **Rod Vagg** *r@va.gg* (9 packages) 39 | - **Mariusz Nowak** *medyk@medikoo.com* (8 packages) 40 | - **Mikeal Rogers** *mikeal.rogers@gmail.com* (8 packages) 41 | - **Shinnosuke Watanabe** *snnskwtnb@gmail.com* (8 packages) 42 | - **Charlie Robbins** *charlie.robbins@gmail.com* (8 packages) 43 | - **moox** *m@moox.io* (8 packages) 44 | - **Vsevolod Strukchinsky** *floatdrop@gmail.com* (8 packages) 45 | - **Max Ogden** *max@maxogden.com* (7 packages) 46 | - **Forbes Lindesay** *forbes@lindesay.co.uk* (7 packages) 47 | - **Linus Unnebäck** *linus@folkdatorn.se* (7 packages) 48 | - **Joshua Appelman** *jappelman@xebia.com* (7 packages) 49 | - **nzakas** *nicholas@nczconsulting.com* (7 packages) 50 | - **Julian Gruber** *julian@juliangruber.com* (7 packages) 51 | - **zcbenz** *zcbenz@gmail.com* (7 packages) 52 | - **geelen** *hi@glenmaddern.com* (6 packages) 53 | - **Jeremiah Senkpiel** *fishrock123@rocketmail.com* (6 packages) 54 | - **qix** *i.am.qix@gmail.com* (6 packages) 55 | - **kevinsawicki** *kevinsawicki@gmail.com* (6 packages) 56 | - **James Nylen** *jnylen@gmail.com* (6 packages) 57 | - **Andrew Goode** *andrewbgoode@gmail.com* (6 packages) 58 | - **Zeke Sikelianos** *zeke@sikelianos.com* (6 packages) 59 | - **Arnout Kazemier** *opensource@3rd-eden.com* (6 packages) 60 | - **develar** *develar@gmail.com* (6 packages) 61 | - **yyx990803** *yyx990803@gmail.com* (6 packages) 62 | - **Eran Hammer** *eran@hammer.io* (6 packages) 63 | - **Paul Miller** *paul+gh@paulmillr.com* (5 packages) 64 | - **Yusuke SUZUKI** *utatane.tea@gmail.com* (5 packages) 65 | - **Felix Geisendörfer** *felix@debuggable.com* (5 packages) 66 | - **Kevin Mårtensson** *kevinmartensson@gmail.com* (5 packages) 67 | - **Simon Boudrias** *admin@simonboudrias.com* (5 packages) 68 | - **Jake Verbaten** *raynos2@gmail.com* (5 packages) 69 | - **Thorsten Lorenz** *thlorenz@gmx.de* (5 packages) 70 | - **Ben Newman** *bn@cs.stanford.edu* (5 packages) 71 | - **Michael Ficarra** *github.public.email@michael.ficarra.me* (5 packages) 72 | - **markdalgleish** *mark.john.dalgleish@gmail.com* (5 packages) 73 | - **Calvin Metcalf** *calvin.metcalf@gmail.com* (5 packages) 74 | - **Simeon Velichkov** *simeonvelichkov@gmail.com* (5 packages) 75 | - **Jarrett Cruger** *jcrugzz@gmail.com* (5 packages) 76 | - **Chris Talkington** (5 packages) 77 | - **AJ ONeal** *awesome@coolaj86.com* (4 packages) 78 | - **Tim Oxley** *secoif@gmail.com* (4 packages) 79 | - **arekinath** *alex@cooperi.net* (4 packages) 80 | - **jlord** *to.jlord@gmail.com* (4 packages) 81 | - **George Zahariev** *z@georgezahariev.com* (4 packages) 82 | - **Glen Maddern** (4 packages) 83 | - **Kyle E. Mitchell** *kyle@kemitchell.com* (4 packages) 84 | - **JP Richardson** *jprichardson@gmail.com* (4 packages) 85 | - **Domenic Denicola** *domenic@domenicdenicola.com* (4 packages) 86 | - **ctalkington** *chris@christalkington.com* (4 packages) 87 | - **Hugh Kennedy** *hughskennedy@gmail.com* (4 packages) 88 | - **Evan You** (4 packages) 89 | - **Maxime Thirouin** (4 packages) 90 | - **Andrew Kelley** *superjoe30@gmail.com* (4 packages) 91 | - **Feross Aboukhadijeh** *feross@feross.org* (4 packages) 92 | - **Aria Minaei** (4 packages) 93 | - **ariaminaei** *aria.minaei@gmail.com* (4 packages) 94 | - **Robert Kieffer** *robert@broofa.com* (4 packages) 95 | - **Vitaly Puzrin** *vitaly@rcdesign.ru* (3 packages) 96 | - **Andres Suarez** *zertosh@gmail.com* (3 packages) 97 | - **Roy Riojas** (3 packages) 98 | - **royriojas** *royriojas@gmail.com* (3 packages) 99 | - **Irakli Gozalishvili** *rfobic@gmail.com* (3 packages) 100 | - **Jordan Harband** *ljharb@gmail.com* (3 packages) 101 | - **dap** *dap@cs.brown.edu* (3 packages) 102 | - **James Coglan** *jcoglan@gmail.com* (3 packages) 103 | - **dominicbarnes** *dominic@dbarnes.info* (3 packages) 104 | - **pfmooney** *patrick.f.mooney@gmail.com* (3 packages) 105 | - **** (3 packages) 106 | - **James Talmage** *james@talmage.io* (3 packages) 107 | - **Matthew Mueller** *matt@lapwinglabs.com* (3 packages) 108 | - **André Cruz** *amdfcruz@gmail.com* (3 packages) 109 | - **freeall** *freeall@gmail.com* (3 packages) 110 | - **Brian White** *mscdex@mscdex.net* (3 packages) 111 | - **dfcreative** *df.creative@gmail.com* (3 packages) 112 | - **Heather Arthur** *fayearthur@gmail.com* (3 packages) 113 | - **Mark Cavage** *mcavage@gmail.com* (3 packages) 114 | - **Robert Kowalski** *rok@kowalski.gd* (3 packages) 115 | - **Arthur Verschaeve** *arthur.versch@gmail.com* (3 packages) 116 | - **Andrey Sitnik** *andrey@sitnik.ru* (3 packages) 117 | - **unshift** *npm@unshift.io* (3 packages) 118 | - **Nodejitsu Inc.** *info@nodejitsu.com* (3 packages) 119 | - **Wyatt Preul** *wpreul@gmail.com* (2 packages) 120 | - **gabelevi** *gabelevi@gmail.com* (2 packages) 121 | - **Ariya Hidayat** *ariya.hidayat@gmail.com* (2 packages) 122 | - **Kris Kowal** *kris@cixar.com* (2 packages) 123 | - **Jason Smith** *jhs@iriscouch.com* (2 packages) 124 | - **Nathan LaFreniere** *quitlahok@gmail.com* (2 packages) 125 | - **Michael Hart** *michael.hart.au@gmail.com* (2 packages) 126 | - **alexindigo** *iam@alexindigo.com* (2 packages) 127 | - **dscape** *nunojobpinto@gmail.com* (2 packages) 128 | - **thejoshwolfe** *thejoshwolfe@gmail.com* (2 packages) 129 | - **"Cowboy" Ben Alman** (2 packages) 130 | - **Fred K. Schott** *fkschott@gmail.com* (2 packages) 131 | - **Federico Romero** *hi@federomero.uy* (2 packages) 132 | - **celer** *dtyree77@gmail.com* (2 packages) 133 | - **maxbrunsfeld** *maxbrunsfeld@gmail.com* (2 packages) 134 | - **bnoordhuis** *info@bnoordhuis.nl* (2 packages) 135 | - **apechimp** *apeherder@gmail.com* (2 packages) 136 | - **Addy Osmani** *addyosmani@gmail.com* (2 packages) 137 | - **David Chambers** *dc@davidchambers.me* (2 packages) 138 | - **Pascal Hartig** *passy@twitter.com* (2 packages) 139 | - **Joyent, Inc** (2 packages) 140 | - **Kevin Sawicki** (2 packages) 141 | - **Mikola Lysenko** (2 packages) 142 | - **mikolalysenko** *mikolalysenko@gmail.com* (2 packages) 143 | - **Ben Alman** *cowboy@rj3.net* (2 packages) 144 | - **Guillermo Rauch** *rauchg@gmail.com* (2 packages) 145 | - **Kyle Robinson Young** *kyle@dontkry.com* (2 packages) 146 | - **Eddie Monge** *eddie+npm@eddiemonge.com* (2 packages) 147 | - **Sam Mikes** *smikes@cubane.com* (2 packages) 148 | - **blakeembrey** *me@blakeembrey.com* (2 packages) 149 | - **Julian Fleischer** (2 packages) 150 | - **scravy** *julian@scravy.de* (2 packages) 151 | - **anthonyshort** *antshort@gmail.com* (2 packages) 152 | - **clintwood** *clint@anotherway.co.za* (2 packages) 153 | - **ianstormtaylor** *ian@ianstormtaylor.com* (2 packages) 154 | - **queckezz** *fabian.eichenberger@gmail.com* (2 packages) 155 | - **stephenmathieson** *me@stephenmathieson.com* (2 packages) 156 | - **thehydroimpulse** *dnfagnan@gmail.com* (2 packages) 157 | - **timaschew** *timaschew@gmail.com* (2 packages) 158 | - **Roman Dvornov** *rdvornov@gmail.com* (2 packages) 159 | - **trevorgerhardt** *trevorgerhardt@gmail.com* (2 packages) 160 | - **Amir Abu Shareb** *yields@icloud.com* (2 packages) 161 | - **michael mifsud** *xzyfer@gmail.com* (2 packages) 162 | - **IndigoUnited** *hello@indigounited.com* (2 packages) 163 | - **Zhiye Li** *github@zhiye.li* (2 packages) 164 | - **rreverser** *me@rreverser.com* (2 packages) 165 | - **benogle** *ogle.ben@gmail.com* (2 packages) 166 | - **Nicholas C. Zakas** *nicholas+npm@nczconsulting.com* (2 packages) 167 | - **atom** *nathan@github.com* (2 packages) 168 | - **Fedor Indutny** *fedor@indutny.com* (2 packages) 169 | - **Parsha Pourkhomami** (2 packages) 170 | - **parshap** *supster+npm@gmail.com* (2 packages) 171 | - **peerigon** *developers@peerigon.com* (2 packages) 172 | - **DC** *threedeecee@gmail.com* (2 packages) 173 | - **brycekahle** *bkahle@gmail.com* (2 packages) 174 | - **Tim** *tim@fostle.com* (2 packages) 175 | - **Carl Xiong** *xiongc05@gmail.com* (2 packages) 176 | - **parshap** *parshap+npm@gmail.com* (2 packages) 177 | - **jhnns** *mail@johannesewald.de* (2 packages) 178 | - **evanw** *evan.exe@gmail.com* (1 package) 179 | - **julien-f** *julien.fontanet@isonoe.net* (1 package) 180 | - **fritx** *uxfritz@163.com* (1 package) 181 | - **Alexander Early** *alexander.early@gmail.com* (1 package) 182 | - **sethlu** (1 package) 183 | - **Matt DesLauriers** *dave.des@gmail.com* (1 package) 184 | - **Paul Betts** *paul@paulbetts.org* (1 package) 185 | - **Beau Gunderson** *beau@beaugunderson.com* (1 package) 186 | - **Caolan McMahon** *caolan.mcmahon@gmail.com* (1 package) 187 | - **cstruct** *albin@mattsson.io* (1 package) 188 | - **James Burke** *jrburke@gmail.com* (1 package) 189 | - **Alex Ford** *alex.ford@codetunnel.com* (1 package) 190 | - **Kiko Beats** *josefrancisco.verdu@gmail.com* (1 package) 191 | - **Alexis Deveria** *adeveria@gmail.com* (1 package) 192 | - **Square, Inc.** (1 package) 193 | - **eventualbuddha** *me@brian-donovan.com* (1 package) 194 | - **Petka Antonov** *petka.antonov@gmail.com* (1 package) 195 | - **ivolodin** *ivolodin@gmail.com* (1 package) 196 | - **byk** *ben@byk.im* (1 package) 197 | - **lo1tuma** *schreck.mathias@gmail.com* (1 package) 198 | - **Luis Couto** *hello@luiscouto.pt* (1 package) 199 | - **couto** *couto@15minuteslate.net* (1 package) 200 | - **Michael Mclaughlin** *M8ch88l@gmail.com* (1 package) 201 | - **benoitz** *bzugmeyer@gmail.com* (1 package) 202 | - **jden** *jason@denizac.org* (1 package) 203 | - **xjamundx** *jamund@gmail.com* (1 package) 204 | - **Adam Bretz** *arbretz@gmail.com* (1 package) 205 | - **marijn** *marijnh@gmail.com* (1 package) 206 | - **lpinca** *luigipinca@gmail.com* (1 package) 207 | - **Jakub Pawlowicz** *contact@jakubpawlowicz.com* (1 package) 208 | - **Aslak Hellesøy** *aslak.hellesoy@gmail.com* (1 package) 209 | - **hacksparrow** *captain@hacksparrow.com* (1 package) 210 | - **jasnell** *jasnell@gmail.com* (1 package) 211 | - **Ben Ripkens** *bripkens.dev@gmail.com* (1 package) 212 | - **null** *jakub@goalsmashers.com* (1 package) 213 | - **Stefan Thomas** *justmoon@members.fsf.org* (1 package) 214 | - **Ruben Bridgewater** *ruben@bridgewater.de* (1 package) 215 | - **Alexis Sellier** *github@cloudhead.io* (1 package) 216 | - **Ramesh Nair** *ram@hiddentao.com* (1 package) 217 | - **Randall Koutnik** *@rkoutnik* (1 package) 218 | - **natevw** *natevw@yahoo.com* (1 package) 219 | - **zloirock** *zloirock@zloirock.ru* (1 package) 220 | - **Joshua Holbrook** *josh.holbrook@gmail.com* (1 package) 221 | - **jhs** *jhs@couchone.com* (1 package) 222 | - **Kent C. Dodds** *kent@doddsfamily.us* (1 package) 223 | - **https://github.com/nearinfinity/node-bplist-parser.git** (1 package) 224 | - **thethomaseffect** *thethomaseffect@gmail.com* (1 package) 225 | - **idralyuk** *igor@buran.us* (1 package) 226 | - **ceejbot** *ceejceej@gmail.com* (1 package) 227 | - **Dave Eddy** *dave@daveeddy.com* (1 package) 228 | - **Philipp Dunkel** *pip@pipobscure.com* (1 package) 229 | - **bajtos** *miro.bajtos@gmail.com* (1 package) 230 | - **Nick Fitzgerald** *nfitzgerald@mozilla.com* (1 package) 231 | - **Strongloop** *callback@strongloop.com* (1 package) 232 | - **mozilla-devtools** *mozilla-developer-tools@googlegroups.com* (1 package) 233 | - **dylanpiercey** *pierceydylan@gmail.com* (1 package) 234 | - **mozilla** *dherman@mozilla.com* (1 package) 235 | - **ctalkington** *chris@talkingtontech.com* (1 package) 236 | - **Alex Wilson** *alex.wilson@joyent.com* (1 package) 237 | - **nickfitzgerald** *fitzgen@gmail.com* (1 package) 238 | - **Ahmad Nassri** *ahmad@ahmadnassri.com* (1 package) 239 | - **Sergey Kryzhanovsky** *skryzhanovsky@ya.ru* (1 package) 240 | - **afelix** *skryzhanovsky@gmail.com* (1 package) 241 | - **watson** *w@tson.dk* (1 package) 242 | - **yoshuawuyts** *i@yoshuawuyts.com* (1 package) 243 | - **tadatuta** *i@tadatuta.com* (1 package) 244 | - **Robert Mustacchi** *rm@fingolfin.org* (1 package) 245 | - **Pierre Curto** (1 package) 246 | - **Michele Bini, Ron Garret, Guy K. Kloss** (1 package) 247 | - **Tom Wu** (1 package) 248 | - **andyperlitch** *andyperlitch@gmail.com* (1 package) 249 | - **Kris Zyp** (1 package) 250 | - **kriszyp** *kriszyp@gmail.com* (1 package) 251 | - **moll** *andri@dot.ee* (1 package) 252 | - **Jan Lehnardt** *jan@apache.org* (1 package) 253 | - **marcbachmann** *marc.brookman@gmail.com* (1 package) 254 | - **pierrec** *pierre.curto@gmail.com* (1 package) 255 | - **Dane Springmeyer** *dane@mapbox.com* (1 package) 256 | - **springmeyer** *dane@dbsgeo.com* (1 package) 257 | - **bergwerkgis** *wb@bergwerk-gis.at* (1 package) 258 | - **mikemorris** *michael.patrick.morris@gmail.com* (1 package) 259 | - **kkaefer** *kkaefer@gmail.com* (1 package) 260 | - **yhahn** *young@developmentseed.org* (1 package) 261 | - **Ben Alpert** *ben@benalpert.com* (1 package) 262 | - **Ryan Day** *soldair@gmail.com* (1 package) 263 | - **Jeremy Stashewsky** *jstashewsky@salesforce.com* (1 package) 264 | - **kossnocorp** *kossnocorp@gmail.com* (1 package) 265 | - **goinstant** *services@goinstant.com* (1 package) 266 | - **TweetNaCl-js contributors** (1 package) 267 | - **dchest** *dmitry@codingrobots.com* (1 package) 268 | - **Ilya Radchenko** *ilya@burstcreations.com* (1 package) 269 | - **joshperry** *josh@6bit.com* (1 package) 270 | - **Stefan Penner** *stefan.penner@gmail.com* (1 package) 271 | - **schnittstabil** *michael@schnittstabil.de* (1 package) 272 | - **ult_combo** *ultcombo@gmail.com* (1 package) 273 | - **jridgewell** *justin+npm@ridgewell.name* (1 package) 274 | - **Aaron Heckmann** *aaron.heckmann+github@gmail.com* (1 package) 275 | - **bevacqua** *nicolasbevacqua@gmail.com* (1 package) 276 | - **Ivan Sagalaev** *maniac@softwaremaniacs.org* (1 package) 277 | - **Greg Allen** *hi@firstandthird.com* (1 package) 278 | - **Lloyd Brookes** *75pound@gmail.com* (1 package) 279 | - **Steve Mao** *maochenyan@gmail.com* (1 package) 280 | - **stevemao** *steve.mao@healthinteract.com.au* (1 package) 281 | - **Juriy "kangax" Zaytsev** (1 package) 282 | - **alexlamsl** *alex+npm@starthq.com* (1 package) 283 | - **kangax** *kangax@gmail.com* (1 package) 284 | - **Charles Blaxland** *charles.blaxland@gmail.com* (1 package) 285 | - **jantimon** *j.nicklas@me.com* (1 package) 286 | - **egeste** *npm@egeste.net* (1 package) 287 | - **Daniel Aristizabal** *aristizabal.daniel@gmail.com* (1 package) 288 | - **ヨーント** *me@yawnt.com* (1 package) 289 | - **HubSpotDev** *devteam@hubspot.com* (1 package) 290 | - **hijonathan** *me@jonathan-kim.com* (1 package) 291 | - **bash** *ashbryanct@gmail.com* (1 package) 292 | - **Pavan Kumar Sunkara** *pavan.sss1991@gmail.com* (1 package) 293 | - **joeferner** *joe@fernsroth.com* (1 package) 294 | - **kael** (1 package) 295 | - **netroy** *aditya@netroy.in* (1 package) 296 | - **Jens Taylor** *jensyt@gmail.com* (1 package) 297 | - **Tyler Kellen** *tyler@sleekcode.net* (1 package) 298 | - **cpojer** *christoph.pojer@gmail.com* (1 package) 299 | - **whitequark** *whitequark@whitequark.org* (1 package) 300 | - **Qix** (1 package) 301 | - **wayfind** (1 package) 302 | - **gjtorikian** *gjtorikian@gmail.com* (1 package) 303 | - **Alex Kocharin** *alex@kocharin.ru* (1 package) 304 | - **Dan Kogai** (1 package) 305 | - **dankogai** *dankogai+github@gmail.com* (1 package) 306 | - **Simon Lydell** (1 package) 307 | - **lydell** *simon.lydell@gmail.com* (1 package) 308 | - **Vladimir Zapparov** *dervus.grim@gmail.com* (1 package) 309 | - **Paul Vorbach** *paul@vorba.ch* (1 package) 310 | - **Aseem Kishore** *aseem.kishore@gmail.com* (1 package) 311 | - **Douglas Crockford** (1 package) 312 | - **Tim Caswell** *tim@creationix.com* (1 package) 313 | - **Jonas Pommerening** *jonas.pommerening@gmail.com* (1 package) 314 | - **Sergey Berezhnoy** *veged@ya.ru* (1 package) 315 | - **jordanbtucker** *jordanbtucker@gmail.com* (1 package) 316 | - **Trent Mick** *trentm@gmail.com* (1 package) 317 | - **veged** *veged@mail.ru* (1 package) 318 | - **arikon** *peimei@ya.ru* (1 package) 319 | - **Steven Levithan** *steves_list@hotmail.com* (1 package) 320 | - **Bower** (1 package) 321 | - **Mat Scales** *mat@wibbly.org.uk* (1 package) 322 | - **Paul Irish** *paul.irish@gmail.com* (1 package) 323 | - **Adam Stankiewicz** *sheerun@sher.pl* (1 package) 324 | - **David DeSandro** *desandrocodes@gmail.com* (1 package) 325 | - **Viacheslav Lotsmanov** *lotsmanov89@gmail.com* (1 package) 326 | - **T. Jameson Little** *t.jameson.little@gmail.com* (1 package) 327 | - **Funerr** *thefunerr@gmail.com* (1 package) 328 | - **Jonathan Rajavuori** *jrajav@gmail.com* (1 package) 329 | - **Benjamin Byholm** *bbyholm@abo.fi* (1 package) 330 | - **Apache CouchDB** *dev@couchdb.apache.org* (1 package) 331 | - **jhs** *jason.h.smith@gmail.com* (1 package) 332 | - **jo** *schmidt@netzmerk.com* (1 package) 333 | - **pgte** *pedro.teixeira@gmail.com* (1 package) 334 | - **Charlie McConnell** *charlie@charlieistheman.com* (1 package) 335 | - **Maciej Małecki** *me@mmalecki.com* (1 package) 336 | - **Matt Lavin** *matt.lavin@gmail.com* (1 package) 337 | - **Andrew Nesbitt** *andrewnez@gmail.com* (1 package) 338 | - **Adeel** *adeelbm@outlook.com* (1 package) 339 | - **Dean Mao** *deanmao@gmail.com* (1 package) 340 | - **Keith Cirkel** *npm@keithcirkel.co.uk* (1 package) 341 | - **Laurent Goderre** *laurent.goderre@gmail.com* (1 package) 342 | - **Marcin Cieślak** *npm@saper.info* (1 package) 343 | - **DY** *dfcreative@gmail.com* (1 package) 344 | - **Meryn Stol** *merynstol@gmail.com* (1 package) 345 | - **Elijah Insua** *tmpvar@gmail.com* (1 package) 346 | - **Sam Roberts** *sam@strongloop.com* (1 package) 347 | - **Nadav Ivgi** (1 package) 348 | - **nadav** *npm@shesek.info* (1 package) 349 | - **trevorburnham** *trevorburnham@gmail.com* (1 package) 350 | - **kat** *kat@lua.cz* (1 package) 351 | - **Devon Govett** *devongovett@gmail.com* (1 package) 352 | - **Tim Koschützki** *tim@debuggable.com* (1 package) 353 | - **Aria Stewart** *aredridel@dinhe.net* (1 package) 354 | - **Graeme Yeates** *yeatesgraeme@gmail.com* (1 package) 355 | - **kenan** *kenan@kenany.me* (1 package) 356 | - **thefourtheye** *thechargingvolcano@gmail.com* (1 package) 357 | - **Marak Squires** (1 package) 358 | - **The Linux Foundation** (1 package) 359 | - **segment** *tj@segment.io* (1 package) 360 | - **dsc** *dsc@less.ly* (1 package) 361 | - **probablycorey** *probablycorey@gmail.com* (1 package) 362 | - **null** *support@marak.com* (1 package) 363 | - **grncdr** *glurgle@gmail.com* (1 package) 364 | - **yisi** *yiorsi@gmail.com* (1 package) 365 | - **Brian J. Brennan** *brianloveswords@gmail.com* (1 package) 366 | - **Drew Young** (1 package) 367 | - **drewyoung1** *drewyoung1@gmail.com* (1 package) 368 | - **Ivan Nikulin** *ifaaan@gmail.com* (1 package) 369 | - **retrofox** *rdsuarez@gmail.com* (1 package) 370 | - **coreh** *thecoreh@gmail.com* (1 package) 371 | - **kelonye** *kelonyemitchel@gmail.com* (1 package) 372 | - **cristiandouce** *cristian@gravityonmars.com* (1 package) 373 | - **swatinem** *arpad.borsos@googlemail.com* (1 package) 374 | - **stagas** *gstagas@gmail.com* (1 package) 375 | - **calvinfo** *calvin@calv.info* (1 package) 376 | - **nami-doc** *vendethiel@hotmail.fr* (1 package) 377 | - **Daniel Cousens** (1 package) 378 | - **dcousens** *email@dcousens.com* (1 package) 379 | - **mreinstein** *reinstein.mike@gmail.com* (1 package) 380 | - **Justineo** *justice360@gmail.com* (1 package) 381 | - **Mark Dalgleish** (1 package) 382 | - **alexgorbatchev** *alex.gorbatchev@gmail.com* (1 package) 383 | - **Christoffer Hallas** *christoffer.hallas@gmail.com* (1 package) 384 | - **Jordan Scales** *scalesjordan@gmail.com* (1 package) 385 | - **Nathan Zadoks** *nathan@nathan7.eu* (1 package) 386 | - **2013+ Bevry Pty Ltd** *us@bevry.me* (1 package) 387 | - **balupton** *b@lupton.cc* (1 package) 388 | - **reconbot** *wizard@roborooter.com* (1 package) 389 | - **spaintrain** *mc.s.pain.how.er+npm@gmail.com* (1 package) 390 | - **Jeff Morrison** *jeff@anafx.com* (1 package) 391 | - **Paul O’Shannessy** *paul@oshannessy.com* (1 package) 392 | - **MoOx** (1 package) 393 | - **'Julian Viereck'** *julian.viereck@gmail.com* (1 package) 394 | - **Steven Vachon** *contact@svachon.com* (1 package) 395 | - **thegoleffect** *thegoleffect@gmail.com* (1 package) 396 | - **Troy Goode** *troygoode@gmail.com* (1 package) 397 | - **julianduque** *julianduquej@gmail.com* (1 package) 398 | - **vbuterin** *vbuterin@gmail.com* (1 package) 399 | - **midnightlightning** *boydb@midnightdesign.ws* (1 package) 400 | - **Cloud Programmability Team** (1 package) 401 | - **mattpodwysocki** *matthew.podwysocki@gmail.com* (1 package) 402 | - **evansolomon** *evan@evanalyze.com* (1 package) 403 | - **Conrad Pankoff** *deoxxa@fknsrs.biz* (1 package) 404 | - **lox** *lachlan@ljd.cc* (1 package) 405 | - **J. Tangelder** (1 package) 406 | - **jtangelder** *j.tangelder@gmail.com* (1 package) 407 | - **akiran** *kiran.coder0@gmail.com* (1 package) 408 | - **celer** *celer@scrypt.net* (1 package) 409 | - **Wes Todd** (1 package) 410 | - **wesleytodd** *wes@wesleytodd.com* (1 package) 411 | - **Artur Adib** *arturadib@gmail.com* (1 package) 412 | - **ariporad** *ari@ariporad.com* (1 package) 413 | - **nfischer** *ntfschr@gmail.com* (1 package) 414 | - **Jeremie Miller** *jeremie@jabber.org* (1 package) 415 | - **Marek Majkowski** (1 package) 416 | - **majek** *majek04@gmail.com* (1 package) 417 | - **msackman** *matthew@rabbitmq.com* (1 package) 418 | - **squaremo** *mikeb@squaremobius.net* (1 package) 419 | - **glasser** *glasser@meteor.com* (1 package) 420 | - **rynomad** *nomad.ry@gmail.com* (1 package) 421 | - **Bryce Kahle** (1 package) 422 | - **Alexandru Marasteanu** *hello@alexei.ro* (1 package) 423 | - **Stefan Judis** (1 package) 424 | - **sebastianhoitz** *hoitz@komola.de* (1 package) 425 | - **Sam Day** *me@samcday.com.au* (1 package) 426 | - **samcday** *sam.c.day@gmail.com* (1 package) 427 | - **Kir Belevich** *kir@soulshine.in* (1 package) 428 | - **greli** *grelimail@gmail.com* (1 package) 429 | - **Gajus Kuizinas** *gajus@gajus.com* (1 package) 430 | - **gajus** *gk@anuary.com* (1 package) 431 | - **Bryce Baril** *bryce@ravenwall.com* (1 package) 432 | - **J. Ryan Stinnett** *jryans@gmail.com* (1 package) 433 | - **KARASZI István** *github@spam.raszi.hu* (1 package) 434 | - **raszi** *npm@spam.raszi.hu* (1 package) 435 | - **Marcel Klehr** *mklehr@gmx.net* (1 package) 436 | - **stefanjudis** *stefanjudis@gmail.com* (1 package) 437 | - **brianloveswords** *brian@nyhacker.org* (1 package) 438 | - **Henrik Joreteg** *henrik@andyet.net* (1 package) 439 | - **Geraint Luff** (1 package) 440 | - **geraintluff** *luffgd@gmail.com* (1 package) 441 | - **bartvds** *bartvanderschoor@gmail.com* (1 package) 442 | - **Microsoft Corp.** (1 package) 443 | - **typescript** *typescript@microsoft.com* (1 package) 444 | - **Mihai Bazon** *mihai.bazon@gmail.com* (1 package) 445 | - **rvanvelzen1** *rvanvelzen1@gmail.com* (1 package) 446 | - **Jeremy Ashkenas** *jashkenas@gmail.com* (1 package) 447 | - **Halász Ádám** *mail@adamhalasz.com* (1 package) 448 | - **Felix Gnass** *fgnass@gmail.com* (1 package) 449 | - **Bjarke Walling** *bwp@bwp.dk* (1 package) 450 | - **Joyent** (1 package) 451 | - **Jared Hanson** *jaredhanson@gmail.com* (1 package) 452 | - **Vincent Voyer** *vincent.voyer@gmail.com* (1 package) 453 | - **Titus Wormer** *tituswormer@gmail.com* (1 package) 454 | - **Sjoerd Visscher** (1 package) 455 | - **Benjamin Thomas** *ben@bentomas.com* (1 package) 456 | - **Dmitrii Karpich** *meettya@gmail.com* (1 package) 457 | - **Christopher Jeffrey (JJ)** *chjjeffrey@gmail.com* (1 package) 458 | - **Alberto Pose** *albertopose@gmail.com* (1 package) 459 | - **Ozgur Ozcitak** *oozcitak@gmail.com* (1 package) 460 | - **jindw** *jindw@xidea.org* (1 package) 461 | - **yaron** *yaronn01@gmail.com* (1 package) 462 | - **bigeasy** *alan@prettyrobots.com* (1 package) 463 | - **kethinov** *kethinov@gmail.com* (1 package) 464 | - **jinjinyun** *jinyun.jin@gmail.com* (1 package) 465 | - **jstash** *jeremy@goinstant.com* (1 package) 466 | 467 | -------------------------------------------------------------------------------- /app/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/dist/.gitkeep -------------------------------------------------------------------------------- /app/electron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, ipcMain, BrowserWindow } = require( 'electron' ); 4 | const path = require( 'path' ); 5 | const ElectronSettings = require( 'electron-settings' ); 6 | const windowStateKeeper = require( 'electron-window-state' ); 7 | const uuid = require( 'uuid' ); 8 | const GitHubApi = require( 'github' ); 9 | const menu = require( './main/menu' ); 10 | const Session = require( './main/session' ); 11 | const repoUtils = require( './main/repoUtils' ); 12 | const createRPC = require( './main/rpc' ); 13 | const github = new GitHubApi( { 14 | protocol : 'https', 15 | headers : { 16 | 'user-agent' : 'Forrest - npm desktop client' 17 | }, 18 | timeout : 5000 19 | } ); 20 | 21 | 22 | // configuration for the available static windows 23 | const initialBgColor = '#f1f1f1'; 24 | const staticWindows = { 25 | about : { 26 | config : { 27 | height : 700, 28 | width : 475, 29 | backgroundColor : initialBgColor, 30 | titleBarStyle : 'hidden', 31 | resizable : false 32 | }, 33 | hash : '#!/about', 34 | initializedWindow : null 35 | }, 36 | help : { 37 | config : { 38 | height : 400, 39 | width : 800, 40 | backgroundColor : initialBgColor, 41 | titleBarStyle : 'hidden', 42 | resizable : false 43 | }, 44 | hash : '#!/help', 45 | initializedWindow : null 46 | }, 47 | updateAvailable : { 48 | config : { 49 | height : 300, 50 | width : 300, 51 | backgroundColor : initialBgColor, 52 | titleBarStyle : 'hidden', 53 | resizable : false 54 | }, 55 | hash : '#!/update-available', 56 | initializedWindow : null 57 | } 58 | }; 59 | 60 | const windowSet = new Set( [] ); 61 | const rpcSet = new Set( [] ); 62 | 63 | let config = {}; 64 | let baseUrl = { 65 | development : '', 66 | production : `file://${ __dirname }/dist/index.html` 67 | }; 68 | 69 | // enable experimental feature to use context menus 70 | app.commandLine.appendSwitch( '--enable-experimental-web-platform-features' ); 71 | 72 | if ( process.env.NODE_ENV === 'development' ) { 73 | config = require( '../config' ).config; 74 | baseUrl.development = `http://localhost:${ config.port }`; 75 | 76 | config.url = `${ baseUrl.development }`; 77 | } else { 78 | config.devtron = false; 79 | config.url = `${ baseUrl.production }`; 80 | } 81 | 82 | ipcMain.on( 'openNewWindow', createWindow ); 83 | 84 | ipcMain.on( 'shellWrite', () => console.log( arguments ) ); 85 | 86 | let settings = new ElectronSettings( { 87 | configFileName : 'npm-app' 88 | } ); 89 | 90 | 91 | /** 92 | * Open a selected static window 93 | * 94 | * @param {String} type - name of the selected window type 95 | */ 96 | function openStaticWindow( type ) { 97 | if ( ! staticWindows[ type ].initializedWindow ) { 98 | staticWindows[ type ].initializedWindow = new BrowserWindow( 99 | staticWindows[ type ].config 100 | ); 101 | 102 | staticWindows[ type ].initializedWindow.loadURL( 103 | `${ config.url }` + staticWindows[ type ].hash 104 | ); 105 | 106 | staticWindows[ type ].initializedWindow.on( 107 | 'closed', () => staticWindows[ type ].initializedWindow = null 108 | ); 109 | } else { 110 | staticWindows[ type ].initializedWindow.focus(); 111 | } 112 | } 113 | 114 | /** 115 | * 116 | */ 117 | function createWindow( event, hash ) { 118 | let mainWindowState = windowStateKeeper( { 119 | defaultWidth : 300, 120 | defaultHeight : 500 121 | } ); 122 | 123 | /** 124 | * Initial window options 125 | */ 126 | let newWindow = new BrowserWindow( { 127 | height : mainWindowState.height, 128 | width : mainWindowState.width, 129 | x : mainWindowState.x, 130 | y : mainWindowState.y, 131 | backgroundColor : initialBgColor, 132 | alwaysOnTop : settings.get( 'app.alwaysOnTop' ), 133 | minWidth : 250, 134 | titleBarStyle : 'hidden', 135 | 'web-preferences' : { 136 | plugins : true 137 | } 138 | } ); 139 | 140 | mainWindowState.manage( newWindow ); 141 | 142 | let url = hash ? `${ config.url }${ hash }` : config.url; 143 | 144 | newWindow.loadURL( url ); 145 | 146 | windowSet.add( newWindow ); 147 | 148 | if ( process.env.NODE_ENV === 'development' ) { 149 | newWindow.webContents.openDevTools( { mode : 'undocked' } ); 150 | } 151 | 152 | if ( config.devtron ) { 153 | BrowserWindow.addDevToolsExtension( path.join( __dirname, '../node_modules/devtron' ) ); 154 | } else { 155 | BrowserWindow.removeDevToolsExtension( 'devtron' ); 156 | } 157 | 158 | const rpc = createRPC( newWindow ); 159 | const sessions = new Map(); 160 | 161 | rpcSet.add( rpc ); 162 | 163 | rpc.on( 'create session', () => { 164 | initSession( { /* rows, cols, cwd, shell */ }, ( uid, session ) => { 165 | sessions.set( uid, session ); 166 | 167 | console.log( 'sesstion created', uid ); 168 | rpc.emit( 'session set', { 169 | uid, 170 | shell : session.shell, 171 | pid : session.pty.pid 172 | } ); 173 | 174 | rpc.emit( 'settings loaded', settings.get( 'app' ) || {} ); 175 | rpc.emit( 'repos loaded', settings.get( 'repos' ) || [] ); 176 | 177 | session.on( 'data', ( data ) => { 178 | rpc.emit( 'session data', { uid, data } ); 179 | } ); 180 | 181 | session.on( 'exit', () => { 182 | rpc.emit( 'session exit', { uid } ); 183 | sessions.delete( uid ); 184 | } ); 185 | } ); 186 | 187 | rpc.on( 'data', ( { uid, data } ) => { 188 | sessions.get( uid ).write( data ); 189 | } ); 190 | 191 | rpc.on( 'update app settings', ( { name, setting } ) => { 192 | settings.set( `app.${ name }`, setting ); 193 | 194 | // TODO save on loop here 195 | windowSet.forEach( ( window ) => { 196 | if ( name === 'alwaysOnTop' ) { 197 | window.setAlwaysOnTop( setting ); 198 | } 199 | } ); 200 | 201 | rpcSet.forEach( rpc => rpc.emit( 'setting set', { name, setting } ) ); 202 | } ); 203 | 204 | rpc.on( 'add repo', ( repoPath ) => { 205 | repoUtils.readRepoData( repoPath ) 206 | .then( repo => { 207 | const savedRepos = settings.get( 'repos' ) || []; 208 | const repos = [ ...savedRepos, repo ]; 209 | 210 | settings.set( 'repos', repos ); 211 | 212 | emitAll( 'repos updated', repos ); 213 | } ) 214 | .catch( ( error ) => { 215 | /* eslint-disable no-console */ 216 | console.log( error ); 217 | } ); 218 | } ); 219 | 220 | rpc.on( 'update repo', ( repoPath ) => { 221 | Promise.all( settings.get( 'repos' ).map( ( repo ) => { 222 | if ( repo.path === repoPath ) { 223 | return repoUtils.readRepoData( repoPath ); 224 | } 225 | 226 | return repo; 227 | } ) ) 228 | .then( repos => { 229 | settings.set( 'repos', repos ); 230 | 231 | emitAll( 'repos updated', repos ); 232 | } ) 233 | .catch( ( error ) => { 234 | /* eslint-disable no-console */ 235 | console.log( error ); 236 | } ); 237 | } ); 238 | 239 | rpc.on( 'remove repo', ( repoPath ) => { 240 | const repos = settings.get( 'repos' ).reduce( ( repos, storedRepo ) => { 241 | if ( storedRepo.path !== repoPath ) { 242 | repos.push( storedRepo ); 243 | } 244 | 245 | return repos; 246 | }, [] ); 247 | 248 | settings.set( 'repos', repos ); 249 | 250 | emitAll( 'repos updated', repos ); 251 | } ); 252 | 253 | rpc.on( 'set terminal size', ( { uid, cols, rows } ) => { 254 | sessions.get( uid ).resize( { cols, rows } ); 255 | } ); 256 | } ); 257 | 258 | 259 | const deleteSessions = () => { 260 | sessions.forEach( ( session, key ) => { 261 | rpc.removeAllListeners( 'data' ); 262 | rpc.removeAllListeners( 'update app settings' ); 263 | rpc.removeAllListeners( 'add repo' ); 264 | rpc.removeAllListeners( 'update repo' ); 265 | rpc.removeAllListeners( 'remove repo' ); 266 | rpc.removeAllListeners( 'set terminal size' ); 267 | 268 | session.removeAllListeners(); 269 | session.destroy(); 270 | sessions.delete( key ); 271 | } ); 272 | }; 273 | 274 | newWindow.on( 'close', () => { 275 | windowSet.delete( newWindow ); 276 | rpcSet.delete( rpc ); 277 | 278 | rpc.destroy(); 279 | deleteSessions(); 280 | } ); 281 | 282 | // we reset the rpc channel only upon 283 | // subsequent refreshes (ie: F5) 284 | newWindow.webContents.on( 'did-navigate', deleteSessions ); 285 | 286 | if ( windowSet.size === 1 ) { 287 | menu.init( { 288 | createWindow, 289 | openStaticWindow 290 | } ); 291 | } 292 | 293 | /* eslint-disable no-console */ 294 | console.log( 'window opened' ); 295 | } 296 | 297 | function emitAll() { 298 | rpcSet.forEach( rpc => rpc.emit.apply( rpc, arguments ) ); 299 | } 300 | 301 | function initSession( opts, fn ) { 302 | fn( uuid.v4(), new Session( opts ) ); 303 | } 304 | 305 | if ( process.env.NODE_ENV !== 'development' ) { 306 | github.repos.getReleases( 307 | { user : 'stefanjudis', repo : 'forrest' }, 308 | ( error, releases ) => { 309 | if ( error ) { 310 | return; 311 | } 312 | 313 | if ( releases.length && releases[ 0 ].tag_name !== `v${ app.getVersion() }` ) { 314 | openStaticWindow( 'updateAvailable' ); 315 | } 316 | } 317 | ); 318 | } 319 | 320 | app.on( 'ready', createWindow ); 321 | 322 | app.on( 'window-all-closed', () => { 323 | if ( process.platform !== 'darwin' ) { 324 | app.quit(); 325 | } 326 | } ); 327 | 328 | app.on( 'activate', () => { 329 | if ( ! windowSet.size ) { 330 | createWindow(); 331 | } 332 | } ); 333 | -------------------------------------------------------------------------------- /app/main.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/main/menu.js: -------------------------------------------------------------------------------- 1 | const electron = require( 'electron' ); 2 | const shell = electron.shell; 3 | const Menu = electron.Menu; 4 | 5 | module.exports = { 6 | init( options ) { 7 | const template = [ 8 | { 9 | label : 'Edit', 10 | submenu : [ 11 | { 12 | role : 'undo' 13 | }, 14 | { 15 | role : 'redo' 16 | }, 17 | { 18 | type : 'separator' 19 | }, 20 | { 21 | role : 'cut' 22 | }, 23 | { 24 | role : 'copy' 25 | }, 26 | { 27 | role : 'paste' 28 | }, 29 | { 30 | role : 'delete' 31 | }, 32 | { 33 | role : 'selectall' 34 | } 35 | ] 36 | }, 37 | { 38 | label : 'View', 39 | submenu : [ 40 | { 41 | label : 'Reload', 42 | accelerator : 'CmdOrCtrl+R', 43 | click( item, focusedWindow ) { 44 | if ( focusedWindow ) focusedWindow.reload(); 45 | } 46 | }, 47 | { 48 | role : 'togglefullscreen' 49 | }, 50 | { 51 | label : 'Toggle Developer Tools', 52 | accelerator : process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', 53 | click( item, focusedWindow ) { 54 | if ( focusedWindow ) { 55 | focusedWindow.webContents.toggleDevTools(); 56 | } 57 | } 58 | } 59 | ] 60 | }, 61 | { 62 | role : 'window', 63 | submenu : [ 64 | { 65 | role : 'minimize' 66 | }, 67 | { 68 | role : 'close' 69 | } 70 | ] 71 | }, 72 | { 73 | role : 'help', 74 | submenu : [ 75 | { 76 | label : 'Shortcuts', 77 | accelerator : 'CmdOrCtrl+/', 78 | click() { options.openStaticWindow( 'help' ); } 79 | }, 80 | { 81 | label : 'Report an Issue', 82 | click() { shell.openExternal( 'https://github.com/stefanjudis/forrest/issues' ); } 83 | } 84 | ] 85 | } 86 | ]; 87 | 88 | if ( process.platform === 'darwin' ) { 89 | const name = electron.app.getName(); 90 | template.unshift( { 91 | label : name, 92 | submenu : [ 93 | { 94 | label : 'About Forrest', 95 | click() { options.openStaticWindow( 'about' ); } 96 | }, 97 | { 98 | type : 'separator' 99 | }, 100 | { 101 | label : 'Open New Window', 102 | accelerator : 'CmdOrCtrl+N', 103 | click() { options.createWindow(); } 104 | }, 105 | { 106 | type : 'separator' 107 | }, 108 | { 109 | label : 'Quit', 110 | role : 'quit' 111 | } 112 | ] 113 | } ); 114 | // Window menu. 115 | template[ 3 ].submenu = [ 116 | { 117 | label : 'Close', 118 | accelerator : 'CmdOrCtrl+W', 119 | role : 'close' 120 | }, 121 | { 122 | label : 'Minimize', 123 | accelerator : 'CmdOrCtrl+M', 124 | role : 'minimize' 125 | }, 126 | { 127 | label : 'Zoom', 128 | role : 'zoom' 129 | } 130 | ]; 131 | } 132 | 133 | const menu = Menu.buildFromTemplate( template ); 134 | 135 | Menu.setApplicationMenu( menu ); 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /app/main/repoUtils.js: -------------------------------------------------------------------------------- 1 | const fs = require( 'fs' ); 2 | 3 | /** 4 | * Evaluate repo url from a read package data 5 | * 6 | * @param {Object} repoData 7 | * 8 | * @returns {null|Object} 9 | */ 10 | function getRepoUrl( repoData ) { 11 | let { repository : repo } = repoData; 12 | 13 | if ( repo ) { 14 | if ( repo.url ) { 15 | return `https://${ repo.url.replace( /((git)?\+?(https)?:\/\/|\.git)/g, '' ) }`; 16 | } 17 | 18 | if ( /^.*?\/.*?$/.test( repo ) ) { 19 | return `https://github.com/${ repo }`; 20 | } 21 | } 22 | 23 | return null; 24 | } 25 | 26 | 27 | /** 28 | * Read repo data 29 | * 30 | * @param {String} repo path 31 | * 32 | * @returns {Promise} 33 | */ 34 | function readRepoData( repoPath ) { 35 | return new Promise( ( resolve, reject ) => { 36 | fs.readFile( `${ repoPath }/package.json`, ( error, data ) => { 37 | if ( error ) { 38 | return reject( error ); 39 | } 40 | 41 | try { 42 | var packageJSON = JSON.parse( data ); 43 | } catch( error ) { 44 | return reject( error ); 45 | } 46 | 47 | let repo = { 48 | path : repoPath, 49 | name : packageJSON.name, 50 | description : packageJSON.description, 51 | url : getRepoUrl( packageJSON ) 52 | }; 53 | 54 | resolve( repo ); 55 | } ); 56 | } ); 57 | } 58 | 59 | module.exports = { 60 | readRepoData 61 | }; 62 | -------------------------------------------------------------------------------- /app/main/rpc.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require( 'events' ); 2 | const { ipcMain } = require( 'electron' ); 3 | const uuid = require( 'uuid' ); 4 | 5 | class Server extends EventEmitter { 6 | 7 | constructor ( win ) { 8 | super(); 9 | this.win = win; 10 | this.ipcListener = this.ipcListener.bind( this ); 11 | 12 | if ( this.destroyed ) { 13 | return; 14 | } 15 | 16 | const uid = uuid.v4(); 17 | this.id = uid; 18 | 19 | ipcMain.on( uid, this.ipcListener ); 20 | 21 | // we intentionally subscribe to `on` instead of `once` 22 | // to support reloading the window and re-initializing 23 | // the channel 24 | this.wc.on( 'did-finish-load', () => { 25 | this.wc.send( 'init', uid ); 26 | } ); 27 | } 28 | 29 | get wc () { 30 | return this.win.webContents; 31 | } 32 | 33 | ipcListener ( event, { ev, data } ) { 34 | super.emit( ev, data ); 35 | } 36 | 37 | emit ( ch, data ) { 38 | this.wc.send( this.id, { ch, data } ); 39 | } 40 | 41 | destroy () { 42 | this.removeAllListeners(); 43 | this.wc.removeAllListeners(); 44 | 45 | if ( this.id ) { 46 | ipcMain.removeListener( this.id, this.ipcListener ); 47 | } else { 48 | // mark for `genUid` in constructor 49 | this.destroyed = true; 50 | } 51 | } 52 | 53 | } 54 | 55 | module.exports = function createRPC ( win ) { 56 | return new Server( win ); 57 | }; 58 | -------------------------------------------------------------------------------- /app/main/session.js: -------------------------------------------------------------------------------- 1 | const { app } = require( 'electron' ); 2 | const { EventEmitter } = require( 'events' ); 3 | const defaultShell = require( 'default-shell' ); 4 | 5 | let spawn = require( 'child_pty' ).spawn; 6 | 7 | module.exports = class Session extends EventEmitter { 8 | constructor () { 9 | super(); 10 | 11 | const baseEnv = Object.assign( {}, process.env, { 12 | LANG : app.getLocale().replace( '-', '_' ) + '.UTF-8', 13 | TERM : 'xterm-256color' 14 | } ); 15 | 16 | this.pty = spawn( defaultShell, [ '--login' ], { 17 | env : baseEnv 18 | } ); 19 | 20 | this.pty.stdout.on( 'data', ( data ) => { 21 | this.emit( 'data', data.toString( 'utf8' ) ); 22 | } ); 23 | 24 | this.pty.on( 'exit', () => { 25 | if ( ! this.ended ) { 26 | this.ended = true; 27 | this.emit( 'exit' ); 28 | } 29 | } ); 30 | 31 | this.write( ' PS1=__FORREST_START__\n RPROMPT=\'\'\n' ); 32 | 33 | this.shell = defaultShell; 34 | } 35 | 36 | exit () { 37 | this.destroy(); 38 | } 39 | 40 | write ( data ) { 41 | this.pty.stdin.write( data ); 42 | } 43 | 44 | resize ( { cols: columns, rows } ) { 45 | try { 46 | this.pty.stdout.resize( { columns, rows } ); 47 | } catch ( error ) { 48 | console.error( error.stack ); 49 | } 50 | } 51 | 52 | destroy () { 53 | try { 54 | this.pty.kill( 'SIGHUP' ); 55 | } catch ( error ) { 56 | console.error( 'exit error', error.stack ); 57 | } 58 | this.emit( 'exit' ); 59 | this.ended = true; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forrest", 3 | "productName": "Forrest", 4 | "version": "1.0.0-zeta", 5 | "description": "The npm script desktop client", 6 | "main": "electron.js", 7 | "dependencies": { 8 | "child_pty": "3.0.1", 9 | "default-shell": "^1.0.1", 10 | "electron-settings": "^1.0.4", 11 | "electron-window-state": "^3.0.3", 12 | "github": "~2.3.0", 13 | "hterm-umdjs": "^1.1.3", 14 | "uuid": "^2.0.2", 15 | "vue": "^1.0.24", 16 | "vue-electron": "^1.0.0", 17 | "vue-router": "^0.7.13", 18 | "vuex": "^0.6.3" 19 | }, 20 | "author": "stefan judis ", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /app/src/App.vue: -------------------------------------------------------------------------------- 1 | 144 | 145 | 157 | 158 | 208 | -------------------------------------------------------------------------------- /app/src/components/AboutView.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 162 | 163 | 172 | -------------------------------------------------------------------------------- /app/src/components/AppView/CustomCommandsInput.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/components/AppView/CustomCommandsInput.vue -------------------------------------------------------------------------------- /app/src/components/AppView/CustomCommandsList.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/components/AppView/CustomCommandsList.vue -------------------------------------------------------------------------------- /app/src/components/AppView/HeaderBar.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 101 | 102 | 111 | -------------------------------------------------------------------------------- /app/src/components/AppView/Settings.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 68 | 69 | 123 | -------------------------------------------------------------------------------- /app/src/components/HelpView.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 184 | -------------------------------------------------------------------------------- /app/src/components/RepoListView.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 39 | 40 | 66 | -------------------------------------------------------------------------------- /app/src/components/RepoListView/KnownRepos.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 141 | 142 | 275 | -------------------------------------------------------------------------------- /app/src/components/RepoListView/OpenRepoButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /app/src/components/RepoView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /app/src/components/RepoView/Command.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 69 | 70 | 87 | -------------------------------------------------------------------------------- /app/src/components/RepoView/CommandOutput.vue: -------------------------------------------------------------------------------- 1 | 2 | 106 | 107 | 137 | 138 | 280 | -------------------------------------------------------------------------------- /app/src/components/RepoView/Repo.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 120 | 121 | 266 | -------------------------------------------------------------------------------- /app/src/components/UpdateAvailableView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 67 | 68 | 77 | -------------------------------------------------------------------------------- /app/src/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | commands : [ 3 | { 4 | name : 'install', 5 | slug : 'default-install', 6 | command : 'npm install', 7 | flags : [], 8 | docs : 'https://docs.npmjs.com/cli/install' 9 | }, 10 | { 11 | name : 'drop & install', 12 | slug : 'default-drop-&-install', 13 | command : 'rm -rf ./node_modules && npm install', 14 | flags : [], 15 | docs : 'https://docs.npmjs.com/cli/install' 16 | }, 17 | { 18 | name : 'shrinkwrap', 19 | slug : 'default-shrinkwrap', 20 | command : 'npm shrinkwrap', 21 | flags : [], 22 | docs : 'https://docs.npmjs.com/cli/shrinkwrap' 23 | }, 24 | { 25 | name : 'list', 26 | slug : 'default-list', 27 | command : 'npm ls', 28 | flags : [], 29 | docs : 'https://docs.npmjs.com/cli/ls' 30 | }, 31 | { 32 | name : 'prune', 33 | slug : 'default-prune', 34 | command : 'npm prune', 35 | flags : [], 36 | docs : 'https://docs.npmjs.com/cli/prune' 37 | }, 38 | { 39 | name : 'outdated', 40 | slug : 'default-outdated', 41 | command : 'npm outdated', 42 | flags : [], 43 | docs : 'https://docs.npmjs.com/cli/outdated' 44 | } 45 | ], 46 | 47 | settings : [ 48 | { 49 | label : 'Stay on top', 50 | desc : 'Should Forrest stay on top of other windows', 51 | name : 'alwaysOnTop', 52 | type : 'checkbox' 53 | }, 54 | { 55 | label : 'Display notifications', 56 | desc : 'Display notifications when a script exits', 57 | name : 'displayNotifications', 58 | type : 'checkbox' 59 | }, 60 | { 61 | label : 'Show project path', 62 | desc : 'Display project path in list view', 63 | name : 'showProjectPath', 64 | type : 'checkbox' 65 | }, 66 | { 67 | label : 'Terminal output font size', 68 | desc : 'Increase/decrease the font size of the terminal output', 69 | name : 'terminalFontSize', 70 | type : 'number', 71 | unit : 'px' 72 | } 73 | ] 74 | }; 75 | -------------------------------------------------------------------------------- /app/src/directives/KeyTracker.vue: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /app/src/directives/OpenExternal.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /app/src/directives/StayDown.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /app/src/fonts/lato/FONTLOG.txt: -------------------------------------------------------------------------------- 1 | 2 | Lato font family 3 | 4 | ================ 5 | 6 | Version 1.104; Western+Polish opensource 7 | 8 | Created by: tyPoland Lukasz Dziedzic 9 | Creation year: 2011 10 | 11 | Copyright (c) 2010-2011 by tyPoland Lukasz Dziedzic with Reserved Font Name "Lato". Licensed under the SIL Open Font License, Version 1.1. 12 | 13 | Lato is a trademark of tyPoland Lukasz Dziedzic. 14 | 15 | Source URL: http://www.latofonts.com/ 16 | License URL: http://scripts.sil.org/OFL 17 | 18 | ================ 19 | 20 | Lato is a sanserif typeface family designed in the Summer 2010 by Warsaw-based designer Łukasz Dziedzic (“Lato” means “Summer” in Polish). In December 2010 the Lato family was published under the open-source Open Font License by his foundry tyPoland, with support from Google. 21 | 22 | In the last ten or so years, during which Łukasz has been designing type, most of his projects were rooted in a particular design task that he needed to solve. With Lato, it was no different. Originally, the family was conceived as a set of corporate fonts for a large client — who in the end decided to go in different stylistic direction, so the family became available for a public release. 23 | 24 | When working on Lato, Łukasz tried to carefully balance some potentially conflicting priorities. He wanted to create a typeface that would seem quite “transparent” when used in body text but would display some original traits when used in larger sizes. He used classical proportions (particularly visible in the uppercase) to give the letterforms familiar harmony and elegance. At the same time, he created a sleek sanserif look, which makes evident the fact that Lato was designed in 2010 — even though it does not follow any current trend. 25 | 26 | The semi-rounded details of the letters give Lato a feeling of warmth, while the strong structure provides stability and seriousness. “Male and female, serious but friendly. With the feeling of the Summer,” says Łukasz. 27 | 28 | Lato consists of five weights (plus corresponding italics), including a beautiful hairline style. 29 | 30 | ================ 31 | 32 | REVISION LOG: 33 | 34 | # Version 1.104 (2011-11-08) 35 | Merged the distribution again 36 | Autohinted with updated ttfautohint 0.4 (which no longer causes Adobe and iOS problems) 37 | except the Hai and Lig weights which are hinted in FLS 5.1. 38 | 39 | # Version 1.102 (2011-10-28) 40 | Added OpenType Layout features 41 | Ssplit between desktop and web versions 42 | Desktop version: all weights autohinted with FontLab Studio 43 | Web version autohinted with ttfautohint 0.4 except the Hai and Lig weights 44 | 45 | # Version 1.101 (2011-09-30) 46 | Fixed OS/2 table Unicode and codepage entries 47 | 48 | # Version 1.100 (2011-09-12) 49 | Added Polish diacritics to the character set 50 | Weights Hai and Lig autohinted with FontLab Studio 51 | Other weights autohinted with ttfautohint 0.3 52 | 53 | # Version 1.011 (2010-12-29) 54 | Added the soft hyphen glyph 55 | 56 | # Version 1.010 (2010-12-13) 57 | Initial version released under SIL Open Font License 58 | Western character set 59 | 60 | ================ 61 | -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-Black.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-BlackItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-BlackItalic.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-Bold.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-BoldItalic.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-Hairline.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-Hairline.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-HairlineItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-HairlineItalic.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-Italic.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-Light.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-LightItalic.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/Lato-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/app/src/fonts/lato/Lato-Regular.woff2 -------------------------------------------------------------------------------- /app/src/fonts/lato/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /app/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Electron from 'vue-electron'; 3 | import Router from 'vue-router'; 4 | import App from './App'; 5 | import routes from './routes'; 6 | 7 | Vue.use( Electron ); 8 | Vue.use( Router ); 9 | 10 | // TODO make this generic 11 | import OpenExternal from './directives/OpenExternal'; 12 | Vue.directive( 'openExternal', OpenExternal ); 13 | 14 | import StayDown from './directives/StayDown'; 15 | Vue.directive( 'stayDown', StayDown ); 16 | 17 | import KeyTracker from './directives/KeyTracker'; 18 | Vue.directive( 'keyTracker', KeyTracker ); 19 | 20 | Vue.config.debug = true; 21 | 22 | const router = new Router(); 23 | 24 | router.map( routes ); 25 | router.beforeEach( () => { 26 | window.scrollTo( 0, 0 ); 27 | } ); 28 | router.redirect( { 29 | '*' : '/' 30 | } ); 31 | 32 | router.start( App, 'app' ); 33 | -------------------------------------------------------------------------------- /app/src/modules/DomUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get parent of dom element with 3 | * given class 4 | * 5 | * @param {Object} el element 6 | * @param {String} className className 7 | * @return {Object} parent element with given class 8 | */ 9 | export function getParentWithClass( el, className ) { 10 | let parent = null; 11 | let p = el.parentElement; 12 | 13 | while ( p !== null ) { 14 | var o = p; 15 | 16 | if ( o.classList && o.classList.contains( className ) ) { 17 | parent = o; 18 | break; 19 | } 20 | 21 | p = o.parentNode; 22 | } 23 | 24 | return parent; 25 | } -------------------------------------------------------------------------------- /app/src/modules/Hterm.js: -------------------------------------------------------------------------------- 1 | import { hterm, lib } from 'hterm-umdjs'; 2 | 3 | hterm.defaultStorage = new lib.Storage.Memory(); 4 | 5 | hterm.Terminal.prototype.overlaySize = function () {}; 6 | 7 | // passthrough all the commands that are meant to control 8 | // hyperterm and not the terminal itself 9 | const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_; 10 | hterm.Keyboard.prototype.onKeyDown_ = function ( event ) { 11 | if ( event.metaKey || event.altKey ) { 12 | return; 13 | } 14 | return oldKeyDown.call( this, event ); 15 | }; 16 | 17 | 18 | // fixes a bug in hterm, where the shorthand hex 19 | // is not properly converted to rgb 20 | // 21 | // thx. @rauchg https://github.com/zeit/hyperterm/blob/master/lib/hterm.js#L91 22 | lib.colors.hexToRGB = function ( arg ) { 23 | var hex16 = lib.colors.re_.hex16; 24 | var hex24 = lib.colors.re_.hex24; 25 | 26 | function convert ( hex ) { 27 | if ( hex.length === 4 ) { 28 | hex = hex.replace( hex16, function( h, r, g, b ) { 29 | return '#' + r + r + g + g + b + b; 30 | } ); 31 | } 32 | var ary = hex.match( hex24 ); 33 | if ( !ary ) return null; 34 | 35 | return 'rgb(' + 36 | parseInt( ary[ 1 ], 16 ) + ', ' + 37 | parseInt( ary[ 2 ], 16 ) + ', ' + 38 | parseInt( ary[ 3 ], 16 ) + 39 | ')'; 40 | } 41 | 42 | if ( arg instanceof Array ) { 43 | for ( var i = 0; i < arg.length; i++ ) { 44 | arg[ i ] = convert( arg[ i ] ); 45 | } 46 | } else { 47 | arg = convert( arg ); 48 | } 49 | 50 | return arg; 51 | }; 52 | 53 | 54 | export default hterm; 55 | export { lib }; 56 | -------------------------------------------------------------------------------- /app/src/modules/Rpc.js: -------------------------------------------------------------------------------- 1 | export default class Client { 2 | constructor () { 3 | const electron = window.require( 'electron' ); 4 | const EventEmitter = window.require( 'events' ); 5 | this.emitter = new EventEmitter(); 6 | this.ipc = electron.ipcRenderer; 7 | this.ipcListener = this.ipcListener.bind( this ); 8 | 9 | if ( window.__rpcId ) { 10 | setTimeout( () => { 11 | this.id = window.__rpcId; 12 | this.ipc.on( this.id, this.ipcListener ); 13 | this.emitter.emit( 'ready' ); 14 | }, 0 ); 15 | } else { 16 | this.ipc.on( 'init', ( ev, uid ) => { 17 | // we cache so that if the object 18 | // gets re-instantiated we don't 19 | // wait for a `init` event 20 | window.__rpcId = uid; 21 | this.id = uid; 22 | 23 | this.ipc.on( uid, this.ipcListener ); 24 | this.emitter.emit( 'ready' ); 25 | } ); 26 | } 27 | } 28 | 29 | ipcListener( event, { ch, data } ) { 30 | this.emitter.emit( ch, data ); 31 | } 32 | 33 | on ( ev, fn ) { 34 | this.emitter.on( ev, fn ); 35 | } 36 | 37 | once ( ev, fn ) { 38 | this.emitter.once( ev, fn ); 39 | } 40 | 41 | emit ( ev, data ) { 42 | if ( ! this.id ) { 43 | throw new Error( 'Not ready' ); 44 | } 45 | 46 | this.ipc.send( this.id, { ev, data } ); 47 | } 48 | 49 | removeListener ( ev, fn ) { 50 | this.emitter.removeListener( ev, fn ); 51 | } 52 | 53 | removeAllListeners () { 54 | this.emitter.removeAllListeners(); 55 | } 56 | 57 | destroy () { 58 | this.removeAllListeners(); 59 | this.ipc.removeAllListeners(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/modules/WindowKeyManager.js: -------------------------------------------------------------------------------- 1 | document.addEventListener( 'keydown', keyDownHandler ); 2 | 3 | export const keyCodes = { 4 | esc : 27, 5 | left : 37, 6 | up : 38, 7 | right : 39, 8 | down : 40, 9 | comma : 188 10 | }; 11 | 12 | const customCommands = [ 'cmdComma' ]; 13 | 14 | const handlerKeys = [ ...Object.keys( keyCodes ), ...customCommands ]; 15 | const handlers = handlerKeys.reduce( ( handlers, key ) => { 16 | handlers[ key ] = { 17 | keyCode : /^cmd.*$/.test( key ) ? 18 | keyCodes[ key.replace( 'cmd', '' ).toLowerCase() ] : 19 | keyCodes[ key ], 20 | handlers : [] 21 | }; 22 | 23 | return handlers; 24 | }, {} ); 25 | 26 | 27 | /** 28 | * Handle keydown events and fire only the last(!) attached event handler 29 | * for given keyCode 30 | * 31 | * @param {Object} event 32 | */ 33 | function keyDownHandler( event ) { 34 | handlerKeys.forEach( key => { 35 | if ( /^cmd.*$/.test( key ) && ! event.metaKey ) { 36 | return; 37 | } 38 | 39 | if ( 40 | handlers[ key ].keyCode === event.keyCode && 41 | handlers[ key ].handlers.length 42 | ) { 43 | handlers[ key ].handlers[ handlers[ key ].handlers.length - 1 ]( event.target ); 44 | 45 | event.preventDefault(); 46 | } 47 | } ); 48 | } 49 | 50 | 51 | /** 52 | * Push a new event handler into the handlers queue 53 | * 54 | * @param {String} key 55 | * @param {Funcation} fn 56 | */ 57 | function add( key, fn ) { 58 | handlers[ key ].handlers.push( fn ); 59 | } 60 | 61 | 62 | /** 63 | * Remove handler from handlers queue 64 | * 65 | * @param {String} key 66 | * @param {Function} fn 67 | */ 68 | function remove( key, fn ) { 69 | handlers[ key ].handlers = handlers[ key ].handlers.reduce( ( handlers, handler ) => { 70 | if ( handler !== fn ) { 71 | handlers.push( handler ); 72 | } 73 | 74 | return handlers; 75 | }, [] ); 76 | } 77 | 78 | export default { 79 | add, 80 | remove 81 | }; -------------------------------------------------------------------------------- /app/src/routes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '/' : { 3 | component : require( './components/RepoListView' ), 4 | name : 'repo-list-page' 5 | }, 6 | '/repos/:repoName' : { 7 | component : require( './components/RepoView' ), 8 | name : 'repo-page' 9 | }, 10 | '/about' : { 11 | component : require( './components/AboutView' ), 12 | name : 'about-page', 13 | isStatic : true 14 | }, 15 | '/help' : { 16 | component : require( './components/HelpView' ), 17 | name : 'help-page', 18 | isStatic : true 19 | }, 20 | '/update-available' : { 21 | component : require( './components/UpdateAvailableView' ), 22 | name : 'update-available-page', 23 | isStatic : true 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/src/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | .u-paddingDefault { 2 | padding : 1em; 3 | } 4 | 5 | .u-noPadding { 6 | padding : 0; 7 | } 8 | 9 | .u-marginHorizontalSmall { 10 | margin-left : .5em; 11 | margin-right : .5em; 12 | } 13 | 14 | .u-negativeMarginHorizontalSmall { 15 | margin-left : -.5em; 16 | margin-right : -.5em; 17 | } 18 | 19 | .u-marginTopSmall { 20 | margin-top : .5em; 21 | } 22 | 23 | .u-marginBottom { 24 | margin-bottom : 1em; 25 | } 26 | 27 | .u-noMarginBottom { 28 | margin-bottom : 0; 29 | } 30 | 31 | .u-negativeBottomMarginTiny { 32 | margin-bottom : -.25em; 33 | } 34 | 35 | .u-marginBottomSmall { 36 | margin-bottom : .5em; 37 | } 38 | 39 | .u-marginRightSmall { 40 | margin-right : .5em; 41 | } 42 | 43 | .u-marginLeftSmall { 44 | margin-left : .5em; 45 | } 46 | 47 | .u-marginTopSmall { 48 | margin-top : .5em; 49 | } 50 | 51 | .u-marginTop { 52 | margin-top : 1em; 53 | } 54 | 55 | .u-marginTopLarge { 56 | margin-top : 2em; 57 | } 58 | 59 | .u-marginLeftAuto { 60 | margin-left : auto; 61 | } 62 | 63 | .u-flex { 64 | display : flex; 65 | } 66 | 67 | .u-flexCenter { 68 | display : flex; 69 | 70 | align-items : center; 71 | justify-content : center; 72 | } 73 | 74 | .u-flexVerticalCenter { 75 | display : flex; 76 | 77 | align-items : center; 78 | } 79 | 80 | .u-flexSpaceBetween { 81 | justify-content : space-between; 82 | } 83 | 84 | .u-flex-50-50 { 85 | display : flex; 86 | 87 | > * { 88 | width : 50%; 89 | } 90 | } 91 | 92 | .u-flex-33-33-33 { 93 | display : flex; 94 | 95 | > * { 96 | width : 33.333%; 97 | } 98 | } 99 | 100 | .u-fillRed { 101 | fill : var(--svg-fill-red); 102 | } 103 | 104 | .u-fillGreen { 105 | fill : var(--svg-fill-green); 106 | } 107 | 108 | .u-fillNone { 109 | fill : none; 110 | } 111 | 112 | .u-fullHeight { 113 | height : 100%; 114 | } 115 | 116 | .u-widthTwoChar { 117 | display : inline-block; 118 | width : 2ch; 119 | } 120 | 121 | .u-visuallyHidden { 122 | border : 0; 123 | clip : rect(0 0 0 0); 124 | height : 1px; 125 | margin : -1px; 126 | overflow : hidden; 127 | padding : 0; 128 | position : absolute; 129 | width : 1px; 130 | } 131 | 132 | .u-positionRelative { 133 | position : relative; 134 | } 135 | 136 | .u-noBorderTop { 137 | border-top : none !important; 138 | } 139 | 140 | .u-fontSizeSmall { 141 | font-size : .9em; 142 | } 143 | 144 | .u-fontWeightBold { 145 | font-weight : bold; 146 | } 147 | 148 | .u-textTransformUppercase { 149 | text-transform : uppercase; 150 | } 151 | -------------------------------------------------------------------------------- /app/src/styles/animations/_move-down.scss: -------------------------------------------------------------------------------- 1 | @keyframes moveDown { 2 | 0% { 3 | transform : translate( 0, 0 ); 4 | } 5 | 6 | 50% { 7 | transform : translate( 0, 25% ); 8 | } 9 | 10 | 100% { 11 | transform : translate( 0, 0 ); 12 | } 13 | } 14 | 15 | .a-moveDown { 16 | animation : .75s moveDown 0s 3 ease-in-out; 17 | } -------------------------------------------------------------------------------- /app/src/styles/components/_drag-handle.scss: -------------------------------------------------------------------------------- 1 | .c-dragHandle { 2 | position : absolute; 3 | 4 | top : 0; 5 | right : 0; 6 | left : 0; 7 | 8 | height : 2em; 9 | 10 | -webkit-user-select : none; 11 | -webkit-app-region : drag; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/styles/components/_logo.scss: -------------------------------------------------------------------------------- 1 | .c-logo { 2 | display : inline-block; 3 | 4 | width : 5em; 5 | height : 5em; 6 | 7 | margin-bottom : .5em; 8 | 9 | svg { 10 | max-width : 100%; 11 | max-height : 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/styles/fonts/_lato.scss: -------------------------------------------------------------------------------- 1 | /* latin-ext */ 2 | @font-face { 3 | font-family: 'Lato'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: local('Lato Light'), local('Lato-Light'), url(./fonts/lato/Lato-Light.woff2) format('woff2'); 7 | } 8 | 9 | /* latin-ext */ 10 | @font-face { 11 | font-family: 'Lato'; 12 | font-style: italic; 13 | font-weight: 300; 14 | src: local('Lato Light Italic'), local('Lato-LightItalic'), url(./fonts/lato/Lato-LightItalic.woff2) format('woff2'); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/styles/objects/_buttons.scss: -------------------------------------------------------------------------------- 1 | .o-linkBtn { 2 | color : inherit; 3 | text-decoration : underline; 4 | line-height : inherit; 5 | font-size : inherit; 6 | font-weight : inherit; 7 | font-family : inherit; 8 | 9 | background : transparent; 10 | 11 | cursor : pointer; 12 | 13 | border : none; 14 | display : inline-block; 15 | } 16 | 17 | .o-primaryBtn { 18 | display : block; 19 | width : 100%; 20 | 21 | padding : .5em; 22 | 23 | color : var(--main-bg-color); 24 | background : var(--npm-red); 25 | 26 | border : none; 27 | 28 | font-size : 1em; 29 | font-family : inherit; 30 | } -------------------------------------------------------------------------------- /app/src/styles/objects/_checkbox.scss: -------------------------------------------------------------------------------- 1 | .o-checkbox { 2 | display : block; 3 | 4 | width : 1.25em; 5 | height : 1.25em; 6 | 7 | background : var(--main-bg-color); 8 | 9 | svg { 10 | width : 100%; 11 | height : 100%; 12 | 13 | transform : scale( 0, 0 ); 14 | transition : transform .125s ease-in-out; 15 | 16 | fill : var(--npm-red); 17 | } 18 | 19 | input:checked + & { 20 | svg { 21 | transition : transform .125s cubic-bezier( 0, 0, 0.25, 1.75 ); 22 | 23 | transform : scale( 1, 1 ); 24 | } 25 | } 26 | 27 | input:focus + & { 28 | outline : .1875em solid var(--npm-red-dark); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/styles/objects/_code.scss: -------------------------------------------------------------------------------- 1 | .o-code { 2 | font-size : .8em; 3 | 4 | padding : .5em; 5 | 6 | background-color : var(--code-background); 7 | color : var(--code-color); 8 | 9 | border-radius : .25em; 10 | 11 | overflow : auto; 12 | 13 | &, & > code { 14 | display : block; 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/styles/objects/_headlines.scss: -------------------------------------------------------------------------------- 1 | .o-headline { 2 | &-1 { 3 | } 4 | 5 | &-2 { 6 | font-size : 1.5em; 7 | font-weight : 600; 8 | } 9 | 10 | &-3 { 11 | font-size : 1.25em; 12 | font-weight : 600; 13 | } 14 | 15 | &-4 { 16 | font-size : 1.125em; 17 | font-weight : bold; 18 | } 19 | 20 | &-5 { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/styles/objects/_icon.scss: -------------------------------------------------------------------------------- 1 | .o-icon { 2 | width : 2.5em; 3 | height : 2.5em; 4 | border : none; 5 | background : none; 6 | padding : .25em; 7 | 8 | svg { 9 | width : 100%; 10 | height : 100%; 11 | 12 | fill : var(--svg-fill); 13 | } 14 | 15 | &:hover { 16 | svg { 17 | fill : var(--npm-red); 18 | } 19 | } 20 | 21 | &:active { 22 | svg { 23 | fill : var(--npm-red-dark); 24 | } 25 | } 26 | 27 | &:disabled { 28 | svg { 29 | fill : #ccc; 30 | } 31 | } 32 | 33 | & .o-pathNoFill { 34 | fill : none; 35 | } 36 | 37 | &.o-icon__npm { 38 | width : 4em; 39 | } 40 | 41 | &.o-icon__fillBright { 42 | svg { 43 | fill : var(--svg-fill-bright); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/styles/objects/_input.scss: -------------------------------------------------------------------------------- 1 | .o-input { 2 | display : block; 3 | width : 100%; 4 | padding : .5em; 5 | border : 1px solid var(--npm-red-dark); 6 | font-size : 1em; 7 | font-family : monospace; 8 | 9 | &:focus { 10 | outline : .1875em solid var(--npm-red-dark); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/styles/objects/_lists.scss: -------------------------------------------------------------------------------- 1 | .o-list { 2 | list-style : none; 3 | 4 | margin : 0; 5 | padding : 0; 6 | 7 | &--item { 8 | padding : .5em .75em; 9 | 10 | &:hover { 11 | background : rgba( 0, 0, 0, .05 ); 12 | } 13 | 14 | & + & { 15 | border-top : 1px solid var(--border-color); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/styles/objects/_paragraphs.scss: -------------------------------------------------------------------------------- 1 | .o-paragraph { 2 | font-size : 1em; 3 | margin-bottom : .5em; 4 | } -------------------------------------------------------------------------------- /app/src/styles/objects/_small.scss: -------------------------------------------------------------------------------- 1 | .o-small { 2 | display : block; 3 | 4 | font-style : italic; 5 | font-weight : 300; 6 | font-size : .8rem; 7 | line-height : 1.375; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/styles/transitions/_slide-down--slide-up.scss: -------------------------------------------------------------------------------- 1 | 2 | .t-slideDown--slideUp-transition { 3 | transition : 4 | transform .275s ease-in-out, 5 | opacity .3s ease-in-out; 6 | 7 | will-change : transform; 8 | transform : translate( 0, 0 ); 9 | opacity : 1; 10 | } 11 | 12 | .t-slideDown--slideUp-enter, .t-slideDown--slideUp-leave { 13 | transform : translate( 0, -100% ); 14 | opacity : .75; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/styles/transitions/_slide-left--slide-right.scss: -------------------------------------------------------------------------------- 1 | .t-slideLeft--slideRight { 2 | &-transition { 3 | transition : 4 | transform .275s ease-in-out, 5 | opacity .3s ease-in-out; 6 | } 7 | 8 | &-enter, &-leave { 9 | transform : translate( 100%, 0 ); 10 | opacity : .3; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/styles/transitions/_slide-right--slide-left.scss: -------------------------------------------------------------------------------- 1 | .t-slideRight--slideLeft { 2 | &-transition { 3 | transition : 4 | transform .275s ease-in-out, 5 | opacity .3s ease-in-out; 6 | } 7 | 8 | &-enter, &-leave { 9 | transform : translate( -100%, 0 ); 10 | opacity : .3; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/styles/transitions/_slide-up--slide-down.scss: -------------------------------------------------------------------------------- 1 | .t-slideUp--slideDown-transition { 2 | transition : 3 | transform .275s ease-in-out, 4 | opacity .3s ease-in-out; 5 | 6 | transform : translate( 0, 0 ); 7 | opacity : 1; 8 | } 9 | 10 | .t-slideUp--slideDown-enter, .t-slideUp--slideDown-leave { 11 | transform : translate( 0, 100% ); 12 | opacity : .75; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/vuex/actions.js: -------------------------------------------------------------------------------- 1 | export const addRepoWithPath = function( { dispatch, state }, repoPath ) { 2 | let repoIsAlreadyAdded = state.repos.all.some( repo => repo.path === repoPath ); 3 | 4 | if ( ! repoIsAlreadyAdded ) { 5 | window.rpc.emit( 6 | 'add repo', 7 | repoPath 8 | ); 9 | } else { 10 | new Notification( 11 | 'Project is already in the list', 12 | { 13 | body : `-> ${ repoPath }` 14 | } 15 | ); 16 | } 17 | }; 18 | 19 | export const reloadRepo = function( { dispatch, state }, repo ) { 20 | window.rpc.emit( 21 | 'update repo', 22 | repo.path 23 | ); 24 | }; 25 | 26 | export const removeRepo = function( { dispatch, state }, repo ) { 27 | window.rpc.emit( 28 | 'remove repo', 29 | repo.path 30 | ); 31 | }; 32 | 33 | export const updateAppSetting = function( { dispatch }, name, setting ) { 34 | window.rpc.emit( 35 | 'update app settings', 36 | { name, setting } 37 | ); 38 | }; 39 | 40 | export const handleUpdatedRepos = function( { dispatch }, repos ) { 41 | dispatch( 'UPDATED_REPOS', repos ); 42 | }; 43 | 44 | export const writeSessionData = function( { dispatch, state }, data ) { 45 | window.rpc.emit( 'data', { uid : state.session.uid, data } ); 46 | }; 47 | 48 | export const execSessionCmd = function( { dispatch, state }, data ) { 49 | // by starting with a space 50 | // we keep it out of the history 51 | data = ` ${ data }\n`; 52 | window.rpc.emit( 'data', { uid : state.session.uid, data } ); 53 | }; 54 | 55 | export const clearSessionData = function( { dispatch, state }, data ) { 56 | dispatch( 'CLEAR_SESSION_OUTPUT', data ); 57 | }; 58 | 59 | export const updateSessionOutput = function( { dispatch }, data ) { 60 | dispatch( 'UPDATE_SESSION_OUTPUT', data ); 61 | }; 62 | 63 | export const setTerminalSize = function( { dispatch, state }, cols, rows ) { 64 | window.rpc.emit( 65 | 'set terminal size', { uid : state.session.uid, cols, rows } 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /app/src/vuex/getters.js: -------------------------------------------------------------------------------- 1 | export function getRepos( state ) { 2 | return state.repos.all; 3 | } 4 | 5 | export function getAppSettings( state ) { 6 | return state.app.settings; 7 | } 8 | 9 | export function isAppReady( state ) { 10 | return state.app.ready; 11 | } 12 | 13 | export function getConfigSettings( state ) { 14 | return state.defaults.settings; 15 | } 16 | 17 | export function getDefaultCommands( state ) { 18 | return state.defaults.commands; 19 | } 20 | 21 | export function getSessionOutput( state ) { 22 | return state.session.output; 23 | } 24 | -------------------------------------------------------------------------------- /app/src/vuex/modules/app.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | settings : {}, 3 | ready : false 4 | }; 5 | 6 | const mutations = { 7 | APP_READY ( state, ready ) { 8 | state.ready = ready; 9 | }, 10 | SETTINGS_LOADED ( state, settings ) { 11 | state.settings = settings; 12 | }, 13 | 14 | UPDATE_APP_SETTING ( state, name, setting ) { 15 | const newSettings = {}; 16 | 17 | newSettings[ name ] = setting; 18 | 19 | state.settings = Object.assign( state.settings, newSettings ); 20 | } 21 | }; 22 | 23 | export default { 24 | state, 25 | mutations 26 | }; 27 | -------------------------------------------------------------------------------- /app/src/vuex/modules/defaults.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '../../defaults'; 2 | 3 | const state = defaultSettings; 4 | 5 | const mutations = {}; 6 | 7 | export default { 8 | state, 9 | mutations 10 | }; 11 | -------------------------------------------------------------------------------- /app/src/vuex/modules/index.js: -------------------------------------------------------------------------------- 1 | const files = require.context( '.', false, /\.js$/ ); 2 | let modules = {}; 3 | 4 | files.keys().forEach( ( key ) => { 5 | if ( key === './index.js' ) return; 6 | modules[ key.replace( /(\.\/|\.js)/g, '' ) ] = files( key ).default; 7 | } ); 8 | 9 | export default modules; 10 | -------------------------------------------------------------------------------- /app/src/vuex/modules/repos.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | all : [] 3 | }; 4 | 5 | const mutations = { 6 | REPOS_LOADED ( state, repos ) { 7 | state.all = repos; 8 | }, 9 | 10 | REPOS_UPDATED ( state, repos ) { 11 | state.all = repos; 12 | } 13 | }; 14 | 15 | export default { 16 | state, 17 | mutations 18 | }; 19 | -------------------------------------------------------------------------------- /app/src/vuex/modules/session.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | id : null, 3 | output : '' 4 | }; 5 | 6 | const mutations = { 7 | CLEAR_SESSION_OUTPUT ( state ) { 8 | state.output = ''; 9 | }, 10 | SET_SESSION ( state, session ) { 11 | state.uid = session.uid; 12 | }, 13 | UPDATE_SESSION_OUTPUT ( state, data ) { 14 | state.output = state.output + data.data; 15 | } 16 | }; 17 | 18 | export default { 19 | state, 20 | mutations 21 | }; 22 | -------------------------------------------------------------------------------- /app/src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import RPC from '../modules/Rpc'; 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | import modules from './modules'; 5 | 6 | Vue.use( Vuex ); 7 | 8 | const rpc = new RPC(); 9 | const store = new Vuex.Store( { 10 | modules, 11 | strict : true 12 | } ); 13 | 14 | console.log( window.rpc ); 15 | 16 | window.__defineGetter__( 'rpc', () => rpc ); 17 | 18 | 19 | // all JS is loaded now it's 20 | // time to spawn the session 21 | rpc.on( 'ready', () => { 22 | rpc.emit( 'create session' ); 23 | } ); 24 | 25 | rpc.on( 'repos loaded', ( repos ) => { 26 | store.dispatch( 'REPOS_LOADED', repos ); 27 | store.dispatch( 'APP_READY', true ); 28 | } ); 29 | 30 | rpc.on( 'repos updated', ( repos ) => { 31 | store.dispatch( 'REPOS_UPDATED', repos ); 32 | } ); 33 | 34 | rpc.on( 'session set', ( session ) => { 35 | store.dispatch( 'SET_SESSION', session ); 36 | } ); 37 | 38 | rpc.on( 'session data', ( data ) => { 39 | store.dispatch( 'UPDATE_SESSION_OUTPUT', data ); 40 | } ); 41 | 42 | rpc.on( 'setting set', ( { name, setting } ) => { 43 | store.dispatch( 'UPDATE_APP_SETTING', name, setting ); 44 | } ); 45 | 46 | rpc.on( 'settings loaded', ( settings ) => { 47 | store.dispatch( 'SETTINGS_LOADED', settings ); 48 | } ); 49 | 50 | export default store; 51 | -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/build/background.png -------------------------------------------------------------------------------- /build/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/build/background@2x.png -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/build/icon.icns -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const pkg = require( './app/package.json' ); 4 | const getDependencies = require( 'dependency-list' ); 5 | 6 | const config = { 7 | name : pkg.name, 8 | devtron : true, 9 | eslint : true, 10 | port : 9080, 11 | vueDevTools : false, 12 | build : { 13 | 'app-version' : pkg.version, 14 | packagesToBeIncluded : [ 'electron-settings', 'electron-window-state', 'github', 'default-shell', 'child_pty', 'uuid' ], 15 | overwrite : true, 16 | asar : false, 17 | dmg : { 18 | 'background-color' : '#E1E1E1', 19 | contents : [ 20 | { 21 | x : 485, 22 | y : 240, 23 | type : 'link', 24 | path : '/Applications' 25 | }, 26 | { 27 | x : 120, 28 | y : 240, 29 | type : 'file' 30 | } 31 | ], 32 | 'icon-size' : 100, 33 | window : { 34 | width : 600, 35 | height : 500 36 | } 37 | }, 38 | } 39 | }; 40 | 41 | /** 42 | * 43 | */ 44 | function getPackConfig( callback ) { 45 | 46 | let versionMap = getPackageVersionsFromApp( config.build.packagesToBeIncluded ); 47 | 48 | // that can be prettier 49 | getDependencies( versionMap, ( error, result ) => { 50 | if ( error ) { 51 | return console.error( error ); 52 | } 53 | 54 | config.build.files = [ 55 | 'electron.js', 56 | 'package.json', 57 | 'main/*', 58 | 'dist/**/*', 59 | '!node_modules/*', 60 | ...Object.keys( result ).map( dep => `node_modules/${ dep }` ) 61 | ]; 62 | 63 | callback( null, config ); 64 | } ); 65 | } 66 | 67 | 68 | /** 69 | * 70 | */ 71 | function getPackageVersionsFromApp( packages ) { 72 | const appPackageJson = require( './app/package' ); 73 | 74 | return packages.reduce( ( versionMap, packageName ) => { 75 | versionMap[ packageName ] = appPackageJson.dependencies[ packageName ]; 76 | 77 | return versionMap; 78 | }, {} ); 79 | } 80 | 81 | 82 | module.exports = { 83 | config : config, 84 | getPackConfig : getPackConfig 85 | }; 86 | -------------------------------------------------------------------------------- /devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-devtools 6 | 30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /devtools/hook.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = "/build/"; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ({ 44 | 45 | /***/ 0: 46 | /***/ function(module, exports, __webpack_require__) { 47 | 48 | eval("'use strict';\n\nvar _hook = __webpack_require__(162);\n\n(0, _hook.installHook)(window);\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaG9vay5qcz80YTAwIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUE7O0FBRUEsdUJBQVksTUFBWiIsImZpbGUiOiIwLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgaW5zdGFsbEhvb2sgfSBmcm9tICcuLi8uLi8uLi9zcmMvYmFja2VuZC9ob29rJ1xuXG5pbnN0YWxsSG9vayh3aW5kb3cpXG5cblxuXG4vKiogV0VCUEFDSyBGT09URVIgKipcbiAqKiAuL3NyYy9ob29rLmpzXG4gKiovIl0sInNvdXJjZVJvb3QiOiIifQ=="); 49 | 50 | /***/ }, 51 | 52 | /***/ 162: 53 | /***/ function(module, exports) { 54 | 55 | eval("'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.installHook = installHook;\n// this script is injected into every page.\n\n/**\n * Install the hook on window, which is an event emitter.\n * Note because Chrome content scripts cannot directly modify the window object,\n * we are evaling this function by inserting a script tag. That's why we have\n * to inline the whole event emitter implementation here.\n *\n * @param {Window} window\n */\n\nfunction installHook(window) {\n var listeners = {};\n\n var hook = {\n Vue: null,\n\n on: function on(event, fn) {\n event = '$' + event;(listeners[event] || (listeners[event] = [])).push(fn);\n },\n once: function once(event, fn) {\n event = '$' + event;\n function on() {\n this.off(event, on);\n fn.apply(this, arguments);\n }\n ;(listeners[event] || (listeners[event] = [])).push(on);\n },\n off: function off(event, fn) {\n event = '$' + event;\n if (!arguments.length) {\n listeners = {};\n } else {\n var cbs = listeners[event];\n if (cbs) {\n if (!fn) {\n listeners[event] = null;\n } else {\n for (var i = 0, l = cbs.length; i < l; i++) {\n var cb = cbs[i];\n if (cb === fn || cb.fn === fn) {\n cbs.splice(i, 1);\n break;\n }\n }\n }\n }\n }\n },\n emit: function emit(event) {\n event = '$' + event;\n var cbs = listeners[event];\n if (cbs) {\n var args = [].slice.call(arguments, 1);\n cbs = cbs.slice();\n for (var i = 0, l = cbs.length; i < l; i++) {\n cbs[i].apply(this, args);\n }\n }\n }\n };\n\n hook.once('init', function (Vue) {\n hook.Vue = Vue;\n });\n\n hook.once('vuex:init', function (store) {\n hook.store = store;\n });\n\n Object.defineProperty(window, '__VUE_DEVTOOLS_GLOBAL_HOOK__', {\n get: function get() {\n return hook;\n }\n });\n}\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vL1VzZXJzL2Jyc3Rld2FyL1JlcG9zaXRvcmllcy9lbGVjdHJvbi1ib2lsZXJwbGF0ZS12dWUvdG9vbHMvdnVlLWRldnRvb2xzL3NyYy9iYWNrZW5kL2hvb2suanM/MjdkNSJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztRQVdnQjs7Ozs7Ozs7Ozs7O0FBQVQsU0FBUyxXQUFULENBQXNCLE1BQXRCLEVBQThCO0FBQ25DLE1BQUksWUFBWSxFQUFaLENBRCtCOztBQUduQyxNQUFNLE9BQU87QUFDWCxTQUFLLElBQUw7O0FBRUEsb0JBQUksT0FBTyxJQUFJO0FBQ2IsY0FBUSxNQUFNLEtBQU4sQ0FESyxDQUVYLFVBQVUsS0FBVixNQUFxQixVQUFVLEtBQVYsSUFBbUIsRUFBbkIsQ0FBckIsQ0FBRCxDQUE4QyxJQUE5QyxDQUFtRCxFQUFuRCxFQUZZO0tBSEo7QUFRWCx3QkFBTSxPQUFPLElBQUk7QUFDZixjQUFRLE1BQU0sS0FBTixDQURPO0FBRWYsZUFBUyxFQUFULEdBQWU7QUFDYixhQUFLLEdBQUwsQ0FBUyxLQUFULEVBQWdCLEVBQWhCLEVBRGE7QUFFYixXQUFHLEtBQUgsQ0FBUyxJQUFULEVBQWUsU0FBZixFQUZhO09BQWY7QUFJQSxPQU5lLENBTWIsVUFBVSxLQUFWLE1BQXFCLFVBQVUsS0FBVixJQUFtQixFQUFuQixDQUFyQixDQUFELENBQThDLElBQTlDLENBQW1ELEVBQW5ELEVBTmM7S0FSTjtBQWlCWCxzQkFBSyxPQUFPLElBQUk7QUFDZCxjQUFRLE1BQU0sS0FBTixDQURNO0FBRWQsVUFBSSxDQUFDLFVBQVUsTUFBVixFQUFrQjtBQUNyQixvQkFBWSxFQUFaLENBRHFCO09BQXZCLE1BRU87QUFDTCxZQUFNLE1BQU0sVUFBVSxLQUFWLENBQU4sQ0FERDtBQUVMLFlBQUksR0FBSixFQUFTO0FBQ1AsY0FBSSxDQUFDLEVBQUQsRUFBSztBQUNQLHNCQUFVLEtBQVYsSUFBbUIsSUFBbkIsQ0FETztXQUFULE1BRU87QUFDTCxpQkFBSyxJQUFJLElBQUksQ0FBSixFQUFPLElBQUksSUFBSSxNQUFKLEVBQVksSUFBSSxDQUFKLEVBQU8sR0FBdkMsRUFBNEM7QUFDMUMsa0JBQUksS0FBSyxJQUFJLENBQUosQ0FBTCxDQURzQztBQUUxQyxrQkFBSSxPQUFPLEVBQVAsSUFBYSxHQUFHLEVBQUgsS0FBVSxFQUFWLEVBQWM7QUFDN0Isb0JBQUksTUFBSixDQUFXLENBQVgsRUFBYyxDQUFkLEVBRDZCO0FBRTdCLHNCQUY2QjtlQUEvQjthQUZGO1dBSEY7U0FERjtPQUpGO0tBbkJTO0FBdUNYLHdCQUFNLE9BQU87QUFDWCxjQUFRLE1BQU0sS0FBTixDQURHO0FBRVgsVUFBSSxNQUFNLFVBQVUsS0FBVixDQUFOLENBRk87QUFHWCxVQUFJLEdBQUosRUFBUztBQUNQLFlBQU0sT0FBTyxHQUFHLEtBQUgsQ0FBUyxJQUFULENBQWMsU0FBZCxFQUF5QixDQUF6QixDQUFQLENBREM7QUFFUCxjQUFNLElBQUksS0FBSixFQUFOLENBRk87QUFHUCxhQUFLLElBQUksSUFBSSxDQUFKLEVBQU8sSUFBSSxJQUFJLE1BQUosRUFBWSxJQUFJLENBQUosRUFBTyxHQUF2QyxFQUE0QztBQUMxQyxjQUFJLENBQUosRUFBTyxLQUFQLENBQWEsSUFBYixFQUFtQixJQUFuQixFQUQwQztTQUE1QztPQUhGO0tBMUNTO0dBQVAsQ0FINkI7O0FBdURuQyxPQUFLLElBQUwsQ0FBVSxNQUFWLEVBQWtCLGVBQU87QUFDdkIsU0FBSyxHQUFMLEdBQVcsR0FBWCxDQUR1QjtHQUFQLENBQWxCLENBdkRtQzs7QUEyRG5DLE9BQUssSUFBTCxDQUFVLFdBQVYsRUFBdUIsaUJBQVM7QUFDOUIsU0FBSyxLQUFMLEdBQWEsS0FBYixDQUQ4QjtHQUFULENBQXZCLENBM0RtQzs7QUErRG5DLFNBQU8sY0FBUCxDQUFzQixNQUF0QixFQUE4Qiw4QkFBOUIsRUFBOEQ7QUFDNUQsd0JBQU87QUFDTCxhQUFPLElBQVAsQ0FESztLQURxRDtHQUE5RCxFQS9EbUMiLCJmaWxlIjoiMTYyLmpzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gdGhpcyBzY3JpcHQgaXMgaW5qZWN0ZWQgaW50byBldmVyeSBwYWdlLlxuXG4vKipcbiAqIEluc3RhbGwgdGhlIGhvb2sgb24gd2luZG93LCB3aGljaCBpcyBhbiBldmVudCBlbWl0dGVyLlxuICogTm90ZSBiZWNhdXNlIENocm9tZSBjb250ZW50IHNjcmlwdHMgY2Fubm90IGRpcmVjdGx5IG1vZGlmeSB0aGUgd2luZG93IG9iamVjdCxcbiAqIHdlIGFyZSBldmFsaW5nIHRoaXMgZnVuY3Rpb24gYnkgaW5zZXJ0aW5nIGEgc2NyaXB0IHRhZy4gVGhhdCdzIHdoeSB3ZSBoYXZlXG4gKiB0byBpbmxpbmUgdGhlIHdob2xlIGV2ZW50IGVtaXR0ZXIgaW1wbGVtZW50YXRpb24gaGVyZS5cbiAqXG4gKiBAcGFyYW0ge1dpbmRvd30gd2luZG93XG4gKi9cblxuZXhwb3J0IGZ1bmN0aW9uIGluc3RhbGxIb29rICh3aW5kb3cpIHtcbiAgbGV0IGxpc3RlbmVycyA9IHt9XG5cbiAgY29uc3QgaG9vayA9IHtcbiAgICBWdWU6IG51bGwsXG5cbiAgICBvbiAoZXZlbnQsIGZuKSB7XG4gICAgICBldmVudCA9ICckJyArIGV2ZW50XG4gICAgICA7KGxpc3RlbmVyc1tldmVudF0gfHwgKGxpc3RlbmVyc1tldmVudF0gPSBbXSkpLnB1c2goZm4pXG4gICAgfSxcblxuICAgIG9uY2UgKGV2ZW50LCBmbikge1xuICAgICAgZXZlbnQgPSAnJCcgKyBldmVudFxuICAgICAgZnVuY3Rpb24gb24gKCkge1xuICAgICAgICB0aGlzLm9mZihldmVudCwgb24pXG4gICAgICAgIGZuLmFwcGx5KHRoaXMsIGFyZ3VtZW50cylcbiAgICAgIH1cbiAgICAgIDsobGlzdGVuZXJzW2V2ZW50XSB8fCAobGlzdGVuZXJzW2V2ZW50XSA9IFtdKSkucHVzaChvbilcbiAgICB9LFxuXG4gICAgb2ZmIChldmVudCwgZm4pIHtcbiAgICAgIGV2ZW50ID0gJyQnICsgZXZlbnRcbiAgICAgIGlmICghYXJndW1lbnRzLmxlbmd0aCkge1xuICAgICAgICBsaXN0ZW5lcnMgPSB7fVxuICAgICAgfSBlbHNlIHtcbiAgICAgICAgY29uc3QgY2JzID0gbGlzdGVuZXJzW2V2ZW50XVxuICAgICAgICBpZiAoY2JzKSB7XG4gICAgICAgICAgaWYgKCFmbikge1xuICAgICAgICAgICAgbGlzdGVuZXJzW2V2ZW50XSA9IG51bGxcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgZm9yIChsZXQgaSA9IDAsIGwgPSBjYnMubGVuZ3RoOyBpIDwgbDsgaSsrKSB7XG4gICAgICAgICAgICAgIGxldCBjYiA9IGNic1tpXVxuICAgICAgICAgICAgICBpZiAoY2IgPT09IGZuIHx8IGNiLmZuID09PSBmbikge1xuICAgICAgICAgICAgICAgIGNicy5zcGxpY2UoaSwgMSlcbiAgICAgICAgICAgICAgICBicmVha1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfSxcblxuICAgIGVtaXQgKGV2ZW50KSB7XG4gICAgICBldmVudCA9ICckJyArIGV2ZW50XG4gICAgICBsZXQgY2JzID0gbGlzdGVuZXJzW2V2ZW50XVxuICAgICAgaWYgKGNicykge1xuICAgICAgICBjb25zdCBhcmdzID0gW10uc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpXG4gICAgICAgIGNicyA9IGNicy5zbGljZSgpXG4gICAgICAgIGZvciAobGV0IGkgPSAwLCBsID0gY2JzLmxlbmd0aDsgaSA8IGw7IGkrKykge1xuICAgICAgICAgIGNic1tpXS5hcHBseSh0aGlzLCBhcmdzKVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgaG9vay5vbmNlKCdpbml0JywgVnVlID0+IHtcbiAgICBob29rLlZ1ZSA9IFZ1ZVxuICB9KVxuXG4gIGhvb2sub25jZSgndnVleDppbml0Jywgc3RvcmUgPT4ge1xuICAgIGhvb2suc3RvcmUgPSBzdG9yZVxuICB9KVxuXG4gIE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh3aW5kb3csICdfX1ZVRV9ERVZUT09MU19HTE9CQUxfSE9PS19fJywge1xuICAgIGdldCAoKSB7XG4gICAgICByZXR1cm4gaG9va1xuICAgIH1cbiAgfSlcbn1cblxuXG5cbi8qKiBXRUJQQUNLIEZPT1RFUiAqKlxuICoqIC9Vc2Vycy9icnN0ZXdhci9SZXBvc2l0b3JpZXMvZWxlY3Ryb24tYm9pbGVycGxhdGUtdnVlL3Rvb2xzL3Z1ZS1kZXZ0b29scy9zcmMvYmFja2VuZC9ob29rLmpzXG4gKiovIl0sInNvdXJjZVJvb3QiOiIifQ=="); 56 | 57 | /***/ } 58 | 59 | /******/ }); 60 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/dist/.gitkeep -------------------------------------------------------------------------------- /media/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/media/logo.jpg -------------------------------------------------------------------------------- /media/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forrest-app/forrest/8a4b3ffe7e64ebbaa29100f2a2bdb15088b03646/media/preview.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Forrest", 3 | "scripts": { 4 | "build": "node tasks/release.js", 5 | "build:clean": "del dist/* !.gitkeep", 6 | "build:darwin": "node tasks/release.js", 7 | "dev": "node tasks/runner.js", 8 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter app", 9 | "pack": "cross-env NODE_ENV=production webpack -p --progress --colors", 10 | "test": "npm run lint", 11 | "postinstall": "install-app-deps", 12 | "vue:route": "node tasks/vue/route.js", 13 | "vuex:module": "node tasks/vuex/module.js" 14 | }, 15 | "author": "Stefan Judis ", 16 | "repository": "stefanjudis/forrest", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "babel-core": "^6.8.0", 20 | "babel-loader": "^6.2.4", 21 | "babel-plugin-transform-runtime": "^6.8.0", 22 | "babel-preset-es2015": "^6.6.0", 23 | "babel-preset-stage-0": "^6.5.0", 24 | "babel-runtime": "^6.11.0", 25 | "cross-env": "^2.0.0", 26 | "css-loader": "^0.23.1", 27 | "dependency-list": "^0.2.2", 28 | "devtron": "^1.1.0", 29 | "electron-builder": "^5.17.0", 30 | "electron-prebuilt": "^1.3.0", 31 | "eslint": "^3.1.1", 32 | "eslint-friendly-formatter": "^2.0.5", 33 | "eslint-loader": "^1.3.0", 34 | "eslint-plugin-html": "^1.4.0", 35 | "eslint-plugin-promise": "^2.0.0", 36 | "extract-text-webpack-plugin": "^1.0.1", 37 | "file-loader": "^0.9.0", 38 | "html-webpack-plugin": "^2.16.1", 39 | "json-loader": "^0.5.4", 40 | "node-sass": "^3.7.0", 41 | "sass-loader": "^4.0.0", 42 | "style-loader": "^0.13.1", 43 | "url-loader": "^0.5.7", 44 | "vue-hot-reload-api": "^1.3.3", 45 | "vue-html-loader": "^1.2.2", 46 | "vue-loader": "^8.3.1", 47 | "vue-style-loader": "^1.0.0", 48 | "webpack": "^1.13.1", 49 | "webpack-dev-server": "^1.14.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tasks/release.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exec = require( 'child_process' ).exec; 4 | const builder = require( 'electron-builder' ); 5 | const Platform = builder.Platform; 6 | 7 | pack(); 8 | 9 | /** 10 | * Build webpack in production 11 | */ 12 | function pack () { 13 | console.log( 'Building webpack in production mode...\n' ); 14 | let pack = exec( 'npm run pack' ); 15 | 16 | pack.stdout.on( 'data', data => console.log( data ) ); 17 | pack.stderr.on( 'data', data => console.error( data ) ); 18 | pack.on( 'exit', code => build() ); 19 | } 20 | 21 | 22 | /** 23 | * Use electron-packager to build electron app 24 | */ 25 | function build () { 26 | require( '../config' ).getPackConfig( ( error, config ) => { 27 | builder.build( { 28 | targets : Platform.MAC.createTarget(), 29 | devMetadata : { 30 | build : config.build 31 | } 32 | } ) 33 | .then( () => { 34 | console.log( 'Build(s) successful!' ); 35 | console.log( 'DONE\n' ); 36 | } ) 37 | .catch( error => { 38 | console.error( error ); 39 | } ); 40 | } ); 41 | } 42 | -------------------------------------------------------------------------------- /tasks/runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Credits to https://github.com/bradstewart/electron-boilerplate-vue/blob/master/build/dev-runner.js 3 | */ 4 | 'use strict'; 5 | 6 | const config = require( '../config' ).config; 7 | const exec = require( 'child_process' ).exec; 8 | 9 | let YELLOW = '\x1b[33m'; 10 | let BLUE = '\x1b[34m'; 11 | let END = '\x1b[0m'; 12 | 13 | let isElectronOpen = false; 14 | 15 | function format ( command, data, color ) { 16 | return color + command + END + 17 | ' ' + // Two space offset 18 | data.toString().trim().replace( /\n/g, '\n' + repeat( ' ', command.length + 2 ) ) + 19 | '\n'; 20 | } 21 | 22 | function repeat ( str, times ) { 23 | return ( new Array( times + 1 ) ).join( str ); 24 | } 25 | 26 | let children = []; 27 | 28 | 29 | /** 30 | * 31 | * 32 | * @param {any} command 33 | * @param {any} color 34 | * @param {any} name 35 | */ 36 | function run ( command, color, name ) { 37 | let child = exec( command ); 38 | 39 | child.stdout.on( 'data', data => { 40 | console.log( format( name, data, color ) ); 41 | 42 | /** 43 | * Start electron after VALID build 44 | * (prevents electron from opening a blank window that requires refreshing) 45 | * 46 | * NOTE: needs more testing for stability 47 | */ 48 | if (/VALID/g.test(data.toString().trim().replace(/\n/g, '\n' + repeat(' ', command.length + 2))) && !isElectronOpen) { 49 | console.log( `${BLUE}Starting electron...\n${END}` ); 50 | run('cross-env NODE_ENV=development electron app/electron.js', BLUE, 'electron') 51 | isElectronOpen = true; 52 | } 53 | } ); 54 | 55 | child.stderr.on( 'data', data => console.error( format( name, data, color ) ) ); 56 | child.on( 'exit', code => exit( code ) ); 57 | 58 | children.push( child ); 59 | } 60 | 61 | /** 62 | * 63 | * 64 | * @param {any} code 65 | */ 66 | function exit ( code ) { 67 | children.forEach( child => { 68 | child.kill(); 69 | } ); 70 | process.exit( code ); 71 | } 72 | 73 | console.log( `${YELLOW}Starting webpack-dev-server...\n${END}` ); 74 | 75 | run( 76 | `webpack-dev-server --inline --hot --colors --port ${config.port} --content-base app/dist`, 77 | YELLOW, 78 | 'webpack' 79 | ); 80 | -------------------------------------------------------------------------------- /tasks/vue/route.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | let routeName = process.argv[2] 7 | let routes = fs.readFileSync( 8 | path.join(__dirname, '../../app/src/routes.js'), 9 | 'utf8' 10 | ).split('\n') 11 | let routeTemplate = fs.readFileSync( 12 | path.join(__dirname, 'route.template.txt'), 13 | 'utf8' 14 | ) 15 | let routesTemplate = fs.readFileSync( 16 | path.join(__dirname, 'routes.template.txt'), 17 | 'utf8' 18 | ) 19 | 20 | routes[routes.length - 3] = routes[routes.length - 3] + ',' 21 | routes.splice( 22 | routes.length - 2, 23 | 0, 24 | routesTemplate 25 | .replace(/{{routeName}}/g, routeName) 26 | .replace(/\n$/, '') 27 | ) 28 | 29 | fs.writeFileSync( 30 | path.join(__dirname, `../../app/src/components/${routeName}View.vue`), 31 | routeTemplate 32 | ) 33 | 34 | fs.mkdirSync(path.join(__dirname, `../../app/src/components/${routeName}View`)) 35 | 36 | fs.writeFileSync( 37 | path.join(__dirname, '../../app/src/routes.js'), 38 | routes.join('\n') 39 | ) 40 | 41 | console.log(`\n\x1b[33m[vue]\x1b[0m route "${routeName}" has been created`) 42 | console.log(' [ \n' + [ 43 | ' ' + path.join(__dirname, `../../app/src/components/${routeName}View.vue`), 44 | path.join(__dirname, `../../app/src/components/${routeName}View`), 45 | path.join(__dirname, '../../app/src/routes.js'), 46 | ].join(',\n ') + '\n ]') 47 | -------------------------------------------------------------------------------- /tasks/vue/route.template.txt: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /tasks/vue/routes.template.txt: -------------------------------------------------------------------------------- 1 | '/{{routeName}}': { 2 | component: require('./components/{{routeName}}View'), 3 | name: '{{routeName}}' 4 | } 5 | -------------------------------------------------------------------------------- /tasks/vuex/module.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | let moduleName = process.argv[2] 7 | let template = fs.readFileSync( 8 | path.join(__dirname, 'module.template.txt'), 9 | 'utf8' 10 | ) 11 | 12 | fs.writeFileSync( 13 | path.join(__dirname, `../../app/src/vuex/modules/${moduleName}.js`), 14 | template 15 | ) 16 | 17 | console.log(`\n\x1b[33m[vuex]\x1b[0m module "${moduleName}" has been created`) 18 | console.log(path.join(__dirname, `../../app/src/vuex/modules/${moduleName}.js`)) 19 | -------------------------------------------------------------------------------- /tasks/vuex/module.template.txt: -------------------------------------------------------------------------------- 1 | import {} from '../mutation-types' 2 | 3 | const state = { 4 | all: [] 5 | } 6 | 7 | const mutations = { 8 | 9 | } 10 | 11 | export default { 12 | state, 13 | mutations 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require( 'path' ); 4 | const settings = require( './config.js' ).config; 5 | const webpack = require( 'webpack' ); 6 | 7 | const ExtractTextPlugin = require( 'extract-text-webpack-plugin' ); 8 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' ); 9 | 10 | let config = { 11 | devtool : '#eval-source-map', 12 | eslint : { 13 | formatter : require( 'eslint-friendly-formatter' ) 14 | }, 15 | entry : { 16 | build : [ path.join( __dirname, 'app/src/main.js' ) ] 17 | }, 18 | module : { 19 | preLoaders : [], 20 | loaders : [ 21 | { 22 | test : /\.css$/, 23 | loader : ExtractTextPlugin.extract( 'style-loader', 'css-loader' ) 24 | }, 25 | { 26 | test : /\.html$/, 27 | loader : 'vue-html-loader' 28 | }, 29 | { 30 | test : /\.js$/, 31 | loader : 'babel', 32 | exclude : /node_modules/ 33 | }, 34 | { 35 | test : /\.json$/, 36 | loader : 'json-loader' 37 | }, 38 | { 39 | test : /\.vue$/, 40 | loader : 'vue-loader' 41 | }, 42 | { 43 | test : /\.(png|jpe?g|gif|svg)(\?.*)?$/, 44 | loader : 'url-loader', 45 | query : { 46 | limit : 10000, 47 | name : 'imgs/[name].[hash:7].[ext]' 48 | } 49 | }, 50 | { 51 | test : /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 52 | loader : 'url-loader', 53 | query : { 54 | limit : 10000, 55 | name : 'fonts/[name].[hash:7].[ext]' 56 | } 57 | } 58 | ] 59 | }, 60 | plugins : [ 61 | new ExtractTextPlugin( 'styles.css' ), 62 | new HtmlWebpackPlugin( { 63 | excludeChunks : [ 'devtools' ], 64 | filename : 'index.html', 65 | template : './app/main.ejs', 66 | title : settings.name 67 | } ), 68 | new webpack.NoErrorsPlugin() 69 | ], 70 | output : { 71 | filename : '[name].js', 72 | path : path.join( __dirname, 'app/dist' ) 73 | }, 74 | resolve : { 75 | alias : { 76 | components : path.join( __dirname, 'app/src/components' ), 77 | src : path.join( __dirname, 'app/src' ) 78 | }, 79 | extensions : [ '', '.js', '.vue', '.json', '.css' ], 80 | fallback : [ path.join( __dirname, 'app/node_modules' ) ] 81 | }, 82 | resolveLoader : { 83 | root : path.join( __dirname, 'node_modules' ) 84 | }, 85 | target : 'electron-renderer', 86 | vue : { 87 | autoprefixer : { 88 | browsers : [ 'last 2 Chrome versions' ] 89 | }, 90 | loaders : { 91 | sass : 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 92 | scss : 'vue-style-loader!css-loader!sass-loader' 93 | } 94 | }, 95 | // TODO check this 96 | externals : [ 'app' ] 97 | }; 98 | 99 | if ( process.env.NODE_ENV !== 'production' ) { 100 | /** 101 | * Apply ESLint 102 | */ 103 | if ( settings.eslint ) { 104 | config.module.preLoaders.push( 105 | { 106 | test : /\.js$/, 107 | loader : 'eslint-loader', 108 | exclude : /node_modules|devtools/ 109 | }, 110 | { 111 | test : /\.vue$/, 112 | loader : 'eslint-loader' 113 | } 114 | ); 115 | } 116 | 117 | /** 118 | * Credits to 119 | * https://github.com/bradstewart/electron-boilerplate-vue/pull/17 120 | * 121 | * Apply vue-devtools window. Is ignored in production mode when building 122 | */ 123 | if ( settings.vueDevTools ) { 124 | config.entry.build.unshift( 125 | path.join( __dirname, 'devtools/hook.js' ), 126 | path.join( __dirname, 'devtools/backend.js' ) 127 | ); 128 | 129 | config.entry.devtools = [ 130 | path.join( __dirname, 'devtools/devtools.js' ) 131 | ]; 132 | 133 | config.plugins.push( new HtmlWebpackPlugin( { 134 | filename : 'devtools.html', 135 | template : path.join( __dirname, 'devtools/devtools.html' ), 136 | excludeChunks : [ 'build' ] 137 | } ) ); 138 | } 139 | } 140 | 141 | /** 142 | * Adjust config for production settings 143 | */ 144 | if ( process.env.NODE_ENV === 'production' ) { 145 | config.devtool = ''; 146 | 147 | config.plugins.push( 148 | new webpack.DefinePlugin( { 149 | 'process.env.NODE_ENV' : '"production"' 150 | } ), 151 | new webpack.optimize.OccurenceOrderPlugin(), 152 | new webpack.optimize.UglifyJsPlugin( { 153 | compress : { 154 | warnings : false 155 | } 156 | } ) 157 | ); 158 | } 159 | 160 | module.exports = config; 161 | --------------------------------------------------------------------------------