├── lib ├── img.scss ├── scss │ ├── dragdrop.scss │ ├── functions.scss │ ├── utils.scss │ ├── mac │ │ ├── mac.scss │ │ └── index.scss │ ├── home.scss │ ├── win │ │ ├── index.scss │ │ └── win.scss │ ├── linux │ │ ├── index.scss │ │ └── linux.scss │ ├── animations.scss │ ├── loading.scss │ ├── progress.scss │ ├── updates.scss │ ├── ace-editor.scss │ ├── tabs.scss │ ├── header.scss │ ├── settings.scss │ ├── variables.scss │ ├── footer.scss │ ├── layout.scss │ ├── table.scss │ └── prompt.scss ├── icons │ └── fontello │ │ ├── font │ │ ├── fontello.eot │ │ ├── fontello.ttf │ │ ├── fontello.woff │ │ ├── fontello.woff2 │ │ └── fontello.svg │ │ ├── LICENSE.txt │ │ ├── css │ │ ├── fontello-codes.css │ │ ├── animation.css │ │ ├── fontello-ie7-codes.css │ │ ├── fontello-ie7.css │ │ └── fontello.css │ │ ├── README.txt │ │ └── config.json ├── js │ ├── directives │ │ ├── ng-autofocus.js │ │ ├── ng-auto-scroll.js │ │ ├── ng-right-click.js │ │ ├── ng-drag-drop.js │ │ ├── ng-resizable.js │ │ ├── ng-table-keyboard.js │ │ ├── ng-ace-editor.js │ │ └── ng-tag-input.js │ ├── filters.js │ ├── errors.js │ ├── notification.js │ ├── loading.js │ ├── npm │ │ ├── npm-runner.js │ │ ├── npm-api.js │ │ └── npm-operations.js │ ├── assets.js │ ├── interface │ │ └── top.js │ ├── index.js │ └── update.js ├── npm-update-log.pug ├── editor-prompt.pug ├── index.pug ├── top.pug ├── install-new-package-version.pug ├── img │ ├── npm-logo-cube.svg │ └── loading.svg ├── footer.pug ├── install-new-package.pug ├── package-informations.pug ├── history-prompt.pug ├── update.pug ├── npm-doctor-log.pug ├── left.pug └── content.pug ├── icon.icns ├── icon.ico ├── gulpfile.js ├── conf ├── paths.json ├── linux.sh └── tasks │ ├── clean.js │ ├── annotate.js │ ├── minify.js │ ├── lint.js │ ├── dist.js │ ├── front-end.js │ └── es6-build.js ├── .gitignore ├── doc ├── ABOUT.md ├── DEVELOP.md ├── BUILD.md ├── CONTRIBUTE.md ├── RECOMMENDATIONS.md └── FAQ.md ├── appveyor.yml ├── .github └── FUNDING.yml ├── .travis.yml ├── ISSUE_TEMPLATE.md ├── .sass-lint.yml ├── index.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json ├── menu.js ├── .eslintrc └── README.md /lib/img.scss: -------------------------------------------------------------------------------- 1 | img[src=""] { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/720kb/ndm/HEAD/icon.icns -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/720kb/ndm/HEAD/icon.ico -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | require('require-dir')('conf/tasks'); 3 | -------------------------------------------------------------------------------- /conf/paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "tmp": ".tmp/", 3 | "lib": "lib/", 4 | "dist": "dist/" 5 | } 6 | -------------------------------------------------------------------------------- /lib/scss/dragdrop.scss: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | &.dragging { 4 | cursor: copy; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/icons/fontello/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/720kb/ndm/HEAD/lib/icons/fontello/font/fontello.eot -------------------------------------------------------------------------------- /lib/icons/fontello/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/720kb/ndm/HEAD/lib/icons/fontello/font/fontello.ttf -------------------------------------------------------------------------------- /lib/icons/fontello/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/720kb/ndm/HEAD/lib/icons/fontello/font/fontello.woff -------------------------------------------------------------------------------- /lib/icons/fontello/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/720kb/ndm/HEAD/lib/icons/fontello/font/fontello.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .tmp/ 3 | etc/ 4 | dist/ 5 | .DS_Store 6 | npm-debug.log 7 | npm-debug* 8 | *.zip 9 | *.dmg 10 | releases/ 11 | releases/* 12 | npm-shrinkwrap.json 13 | -------------------------------------------------------------------------------- /lib/scss/functions.scss: -------------------------------------------------------------------------------- 1 | @mixin bg-rgba-white($opacity) { 2 | background-color: rgba(255, 255, 255, $opacity); 3 | } 4 | @mixin bg-rgba-black($opacity) { 5 | background-color: rgba(0, 0, 0, $opacity); 6 | } 7 | -------------------------------------------------------------------------------- /conf/linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -F package=@releases/ndm_$(echo $1)_amd64.deb https://$2@push.fury.io/720kb/ && \ 4 | curl -F package=@releases/ndm-$(echo $1).rpm https://$2@push.fury.io/720kb/ && \ 5 | 6 | echo "Done." 7 | -------------------------------------------------------------------------------- /conf/tasks/clean.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | const gulp = require('gulp') 3 | , del = require('del') 4 | , paths = require('../paths.json'); 5 | 6 | gulp.task('clean', () => { 7 | 8 | return del([ 9 | paths.tmp, 10 | paths.dist 11 | ]); 12 | }); 13 | -------------------------------------------------------------------------------- /lib/scss/utils.scss: -------------------------------------------------------------------------------- 1 | .overflow-x-hidden { 2 | overflow-x: hidden; 3 | } 4 | 5 | .out-of-screen { 6 | position: absolute; 7 | z-index: -1; 8 | } 9 | 10 | .separator10 { 11 | clear: both; 12 | float: none; 13 | min-height: 10px; 14 | width: 100%; 15 | } 16 | 17 | .action-link-disabled { 18 | opacity: .5; 19 | } 20 | -------------------------------------------------------------------------------- /conf/tasks/annotate.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | const gulp = require('gulp') 3 | , ngAnnotate = require('gulp-ng-annotate') 4 | , paths = require('../paths.json'); 5 | 6 | gulp.task('annotate', ['es6-build'], () => { 7 | 8 | return gulp.src(`${paths.tmp}**/*.js`) 9 | .pipe(ngAnnotate()) 10 | .pipe(gulp.dest(`${paths.tmp}`)); 11 | }); 12 | -------------------------------------------------------------------------------- /lib/scss/mac/mac.scss: -------------------------------------------------------------------------------- 1 | body, html { 2 | cursor: default; 3 | font-family: '-apple-system', 'BlinkMacSystemFont', 'Helvetica Neue', 'Arial', 'sans-serif'; 4 | } 5 | 6 | * { 7 | outline: none; 8 | text-decoration: none; 9 | cursor: default; 10 | 11 | &:hover, &:active, &:focus { 12 | outline: none; 13 | text-decoration: none; 14 | cursor: default; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/scss/home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | min-height: calc(100vh - 37px); 3 | padding-top: 29vh; 4 | text-align: center; 5 | position: relative; 6 | z-index: 9; 7 | background: white; 8 | border: 1px solid #ccc; 9 | margin: 0 auto; 10 | 11 | button { 12 | height: 21px; 13 | font-size: 12.5px; 14 | width: 105px; 15 | } 16 | small { 17 | font-size: 13px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/js/directives/ng-autofocus.js: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.autofocus'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngAutofocus', /*@ngInject*/ function ngAutofocus() { 7 | return (scope, element) => { 8 | scope.$evalAsync(() => { 9 | element[0].focus(); 10 | }); 11 | }; 12 | }); 13 | 14 | export default moduleName; 15 | -------------------------------------------------------------------------------- /doc/ABOUT.md: -------------------------------------------------------------------------------- 1 | ## About ndm 2 | 3 | **ndm** stands for _"npm desktop manager"_ 4 | 5 | A cross-platform GUI for [npm](https://npmjs.com/) built with web technologies. 6 | 7 | With **ndm** you can manage npm, npm projects and packages straight from the couch. 8 | 9 | **ndm** is packed up thanks to [Electron](https://github.com/electron/electron) and developed in HTML/CSS/JS powered by AngularJS, Sass and Pug. 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/js/directives/ng-auto-scroll.js: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-autoscroll'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngAutoscroll', /*@ngInject*/ function ngAutoscroll() { 7 | return (scope, element) => { 8 | scope.$watch(() => { 9 | element[0].scrollTop = element[0].scrollHeight; 10 | }); 11 | }; 12 | }); 13 | 14 | export default moduleName; 15 | -------------------------------------------------------------------------------- /doc/DEVELOP.md: -------------------------------------------------------------------------------- 1 | 2 | ## Develop ndm 3 | 4 | ### Setup 5 | 6 | `$ git clone https://github.com/720kb/ndm.git` 7 | 8 | `$ cd ndm` 9 | 10 | `$ npm install` 11 | 12 | ### Run app 13 | 14 | #### Run on Linux 15 | `$ npm run linux` 16 | 17 | #### Run on Mac 18 | `$ npm run mac` 19 | 20 | #### Run on Windows 21 | `$ npm run win` 22 | 23 | 24 | if you then want to test the executables follow [How to build](https://github.com/720kb/ndm/blob/master/doc/BUILD.md) 25 | 26 | -------------------------------------------------------------------------------- /lib/scss/mac/index.scss: -------------------------------------------------------------------------------- 1 | @import '../animations'; 2 | @import '../functions'; 3 | @import '../variables'; 4 | @import '../utils'; 5 | @import '../settings'; 6 | @import '../layout'; 7 | @import '../header'; 8 | @import '../table'; 9 | @import '../footer'; 10 | @import '../prompt'; 11 | @import '../loading'; 12 | @import '../progress'; 13 | @import '../dragdrop'; 14 | @import '../ace-editor'; 15 | @import '../home'; 16 | @import '../updates'; 17 | @import '../tabs'; 18 | @import 'mac'; 19 | -------------------------------------------------------------------------------- /lib/scss/win/index.scss: -------------------------------------------------------------------------------- 1 | @import '../animations'; 2 | @import '../functions'; 3 | @import '../variables'; 4 | @import '../utils'; 5 | @import '../settings'; 6 | @import '../layout'; 7 | @import '../header'; 8 | @import '../table'; 9 | @import '../footer'; 10 | @import '../prompt'; 11 | @import '../loading'; 12 | @import '../progress'; 13 | @import '../dragdrop'; 14 | @import '../ace-editor'; 15 | @import '../home'; 16 | @import '../updates'; 17 | @import '../tabs'; 18 | @import 'win'; 19 | -------------------------------------------------------------------------------- /lib/scss/linux/index.scss: -------------------------------------------------------------------------------- 1 | @import '../animations'; 2 | @import '../functions'; 3 | @import '../variables'; 4 | @import '../utils'; 5 | @import '../settings'; 6 | @import '../layout'; 7 | @import '../header'; 8 | @import '../table'; 9 | @import '../footer'; 10 | @import '../prompt'; 11 | @import '../loading'; 12 | @import '../progress'; 13 | @import '../dragdrop'; 14 | @import '../ace-editor'; 15 | @import '../home'; 16 | @import '../updates'; 17 | @import '../tabs'; 18 | @import 'linux'; 19 | -------------------------------------------------------------------------------- /conf/tasks/minify.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | const gulp = require('gulp') 3 | , runSequence = require('run-sequence') 4 | , paths = require('../paths.json') 5 | , minifyJS = require('gulp-uglify'); 6 | 7 | gulp.task('distify', done => { 8 | 9 | runSequence( 10 | 'dist', 11 | 'dist-minify-js', 12 | done); 13 | }); 14 | 15 | gulp.task('dist-minify-js', () => { 16 | 17 | return gulp.src(`${paths.dist}js/*.js`) 18 | .pipe(minifyJS()) 19 | .pipe(gulp.dest(`${paths.dist}js/`)); 20 | }); 21 | -------------------------------------------------------------------------------- /conf/tasks/lint.js: -------------------------------------------------------------------------------- 1 | /*global __dirname,require*/ 2 | 3 | const gulp = require('gulp') 4 | , eslint = require('gulp-eslint') 5 | , path = require('path') 6 | , paths = require('../paths.json') 7 | , toLint = path.resolve(__dirname, '../..', paths.lib, '**/*.js') 8 | , gulpFolder = path.resolve(__dirname, '**/*.js'); 9 | 10 | gulp.task('lint', () => { 11 | 12 | return gulp.src([gulpFolder, toLint]) 13 | .pipe(eslint()) 14 | .pipe(eslint.format()) 15 | .pipe(eslint.failOnError()); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/scss/win/win.scss: -------------------------------------------------------------------------------- 1 | body, html { 2 | cursor: default; 3 | font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; 4 | } 5 | 6 | a, button, .fake-link { 7 | cursor: pointer; 8 | &:hover, &:active, &:focus { 9 | cursor: pointer; 10 | } 11 | } 12 | 13 | .page { 14 | 15 | box-shadow: 0 1px 0 $color-ccc inset; 16 | } 17 | 18 | .home { 19 | button { 20 | height: 23px; 21 | } 22 | } 23 | 24 | .dialog { 25 | select { 26 | vertical-align: text-top; 27 | } 28 | } 29 | 30 | .ace_editor { 31 | font-size: 14px !important; 32 | } 33 | -------------------------------------------------------------------------------- /lib/scss/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes loadingStripes { 2 | from { 3 | background-position: 0 0; 4 | } 5 | 6 | to { 7 | background-position: 50px 0; 8 | } 9 | } 10 | @keyframes promptSliding { 11 | 0% { 12 | transform: translateY(-5px); 13 | } 14 | 15 | 100% { 16 | transform: translateY(0); 17 | } 18 | } 19 | 20 | @keyframes toggleOpacity { 21 | 22 | 0% { 23 | opacity: .2; 24 | } 25 | 26 | 100% { 27 | opacity: 1; 28 | } 29 | } 30 | @keyframes toggleOpacityInverse { 31 | 32 | 0% { 33 | opacity: 1; 34 | } 35 | 36 | 100% { 37 | opacity: .2; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/scss/loading.scss: -------------------------------------------------------------------------------- 1 | body { 2 | .app-big-loading { 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | text-align: center; 7 | background: $bg-light; 8 | min-width: 100vw; 9 | min-height: 100vh; 10 | z-index: 99999; 11 | 12 | img { 13 | width: 33px; 14 | margin-top: 40vh; 15 | opacity: .6; 16 | } 17 | } 18 | 19 | &.freezed { 20 | opacity: .5; 21 | pointer-events: none; 22 | * { 23 | pointer-events: none; 24 | } 25 | } 26 | 27 | &.ready { 28 | .app-big-loading { 29 | display: none; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/js/directives/ng-right-click.js: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-right-click'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngRightClick', /*@ngInject*/ function ngRightClick($parse) { 7 | return (scope, element, attrs) => { 8 | 9 | element.on('contextmenu', event => { 10 | scope.$apply(() => { 11 | event.preventDefault(); 12 | let fn = $parse(attrs.ngRightClick); 13 | 14 | fn(scope, { 15 | '$event': event 16 | }); 17 | }); 18 | }); 19 | }; 20 | }); 21 | 22 | export default moduleName; 23 | -------------------------------------------------------------------------------- /lib/scss/linux/linux.scss: -------------------------------------------------------------------------------- 1 | body, html { 2 | cursor: default; 3 | font-family: 'Oxygen', 'Ubuntu', 'Cantarell', 'Arial', 'sans-serif'; 4 | } 5 | 6 | * { 7 | outline: none; 8 | text-decoration: none; 9 | cursor: default; 10 | 11 | &:hover, &:active, &:focus { 12 | outline: none; 13 | text-decoration: none; 14 | cursor: default; 15 | } 16 | } 17 | 18 | .home { 19 | button { 20 | height: 23px; 21 | } 22 | } 23 | 24 | .footer { 25 | button { 26 | font-size: 10.5px; 27 | } 28 | } 29 | .ace_editor { 30 | font-size: 15px !important; 31 | } 32 | 33 | .dialog { 34 | 35 | select { 36 | height: 18.5px; 37 | bottom: 0; 38 | top: -1px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/icons/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2016 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | ## Entypo 14 | 15 | Copyright (C) 2012 by Daniel Bruce 16 | 17 | Author: Daniel Bruce 18 | License: SIL (http://scripts.sil.org/OFL) 19 | Homepage: http://www.entypo.com 20 | 21 | 22 | ## Modern Pictograms 23 | 24 | Copyright (c) 2012 by John Caserta. All rights reserved. 25 | 26 | Author: John Caserta 27 | License: SIL (http://scripts.sil.org/OFL) 28 | Homepage: http://thedesignoffice.org/project/modern-pictograms/ 29 | 30 | 31 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | #environment: 2 | # nodejs_version: '6' 3 | 4 | platform: 5 | - x86 6 | 7 | install: 8 | #- ps: Install-Product node $env:nodejs_version 9 | - echo 'NODE_VERSION' && node -v 10 | - echo 'NPM_VERSION' && npm -v 11 | - appveyor-retry npm install 12 | - npm run build-win 13 | - ps: get-childItem releases\*.exe | rename-item -newname { $_.name -replace " Setup ","-" } 14 | 15 | build: off 16 | test: off 17 | 18 | artifacts: 19 | - path: releases\*.exe 20 | - path: releases\*win.zip 21 | 22 | deploy: 23 | - provider: GitHub 24 | auth_token: 25 | secure: ZQe3awDIx7JDSXPfjp8Y+mKgdXERdHAeNikOKo3eyYWZzjykn74n6S6B8qJqqbBx 26 | draft: false 27 | prerelease: false 28 | on: 29 | appveyor_repo_tag: true 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode8.2 3 | language: node_js 4 | before_install: 5 | - brew update 6 | - brew install gnu-tar graphicsmagick rpm 7 | node_js: 8 | - "6" 9 | script: 10 | - npm run lint 11 | - npm run build-mac 12 | - npm run build-linux 13 | - export VERSION=$(echo $TRAVIS_TAG | tr -d "v") 14 | deploy: 15 | - provider: releases 16 | api_key: $GITHUB_ACCESS_TOKEN 17 | file: 18 | - "releases/ndm-$(echo $VERSION).dmg" 19 | - "releases/ndm-$(echo $VERSION)-mac.zip" 20 | - "releases/ndm-$(echo $VERSION).zip" 21 | skip_cleanup: true 22 | on: 23 | tags: true 24 | - provider: script 25 | script: conf/linux.sh $VERSION $GEMFURY_TOKEN 26 | skip_cleanup: true 27 | on: 28 | tags: true 29 | -------------------------------------------------------------------------------- /lib/js/filters.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | const moduleName = 'npm-ui.filters'; 3 | 4 | angular.module(moduleName, []) 5 | .filter('removeHTML', () => { 6 | return string => { 7 | return string.replace(/<\/?[^>]+(>|$)/g, ''); 8 | }; 9 | }) 10 | .filter('lastNameInPath', () => { 11 | return string => { 12 | let toReturn 13 | , split; 14 | 15 | if (string.includes('\\')) { 16 | //on windows 17 | split = string.split('\\'); 18 | toReturn = split[split.length - 1]; 19 | } 20 | 21 | if (string.includes('/')) { 22 | //on linux and mac 23 | split = string.split('/'); 24 | toReturn = split[split.length - 1]; 25 | } 26 | return toReturn ? toReturn : string; 27 | }; 28 | }); 29 | 30 | export default moduleName; 31 | -------------------------------------------------------------------------------- /lib/js/directives/ng-drag-drop.js: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-drag-drop'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngDragDrop', /*@ngInject*/ function ngDragAndDrop($rootScope) { 7 | return (scope, element) => { 8 | element.on('drop', event => { 9 | event.preventDefault(); 10 | element.removeClass('dragging'); 11 | $rootScope.$emit('shell:file-drop', event); 12 | }); 13 | element.on('dragover', event => { 14 | event.preventDefault(); 15 | element.addClass('dragging'); 16 | }); 17 | element.on('dragleave', event => { 18 | event.preventDefault(); 19 | element.removeClass('dragging'); 20 | }); 21 | }; 22 | }); 23 | 24 | export default moduleName; 25 | -------------------------------------------------------------------------------- /lib/npm-update-log.pug: -------------------------------------------------------------------------------- 1 | div.dialog.dialog-window(ng-if="shell.activeLink === 'update'") 2 | div(class="prompt-window-options") 3 | img(ng-show="shell.updatingNpm", src='img/loading.svg', width='13') 4 | i(class="fa fa-check color-primary", ng-show="!shell.updatingNpm && log.logs") 5 | button(ng-click="shell.activeLink = false") 6 | | Close 7 | button(ng-click="shell.activeClickedLink('update'); shell.updateNpm()", ng-if="!shell.updatingNpm") 8 | | Run again 9 | div(contenteditable="true", class="window", ng-autoscroll) 10 | div(class="col-md-12 logs") 11 | b 12 | | Updating npm 13 | div(class="col-md-12 logs") 14 | | Output: 15 | div(class="col-md-12 logs", contenteditable="true", ng-repeat="aLog in log.logs track by $index") 16 | | {{ aLog }} 17 | -------------------------------------------------------------------------------- /conf/tasks/dist.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | const gulp = require('gulp') 3 | , runSequence = require('run-sequence') 4 | , paths = require('../paths.json'); 5 | 6 | gulp.task('dist', ['annotate'], done => { 7 | 8 | runSequence([ 9 | 'copy-transpiled-js-files', 10 | 'copy-img-files', 11 | 'copy-icon-files' 12 | ], done); 13 | }); 14 | 15 | gulp.task('copy-transpiled-js-files', () => { 16 | 17 | return gulp.src(`${paths.tmp}**/*`) 18 | .pipe(gulp.dest(`${paths.dist}`)); 19 | }); 20 | 21 | gulp.task('copy-img-files', () => { 22 | 23 | return gulp.src(`${paths.lib}img/**/*`) 24 | .pipe(gulp.dest(`${paths.dist}img`)); 25 | }); 26 | 27 | gulp.task('copy-icon-files', () => { 28 | 29 | return gulp.src(`${paths.lib}icons/**/*`) 30 | .pipe(gulp.dest(`${paths.dist}icons`)); 31 | }); 32 | -------------------------------------------------------------------------------- /doc/BUILD.md: -------------------------------------------------------------------------------- 1 | 2 | ## Build the app 3 | 4 | Generate the Desktop executables which you can run whitout needing to open the terminal (.dmg, .deb, .exe, etc ..) 5 | 6 | #### Setup 7 | 8 | ``` 9 | $ git clone https://github.com/720kb/ndm.git 10 | 11 | $ cd ndm 12 | 13 | $ npm install 14 | ``` 15 | 16 | 17 | #### Builds for Mac 18 | 19 | `$ npm run build-mac` 20 | 21 | #### Builds for Linux 22 | 23 | `$ npm run build-linux` 24 | 25 | #### Builds for Windows 26 | 27 | `$ npm run build-win` 28 | 29 | #### Builds for all the platforms 30 | 31 | `$ npm run build` 32 | 33 | 34 | The executables are generated thanks to the [electron-builder](https://github.com/electron-userland/electron-builder), if you want you can change the build settings to your needs, just follow their documentation. 35 | 36 | The executables will be generated inside the `/releases` folder. 37 | -------------------------------------------------------------------------------- /lib/editor-prompt.pug: -------------------------------------------------------------------------------- 1 | div.dialog.dialog-window(ng-if="leftBar.editorFilePath", ng-ace-editor, ng-ace-editor-theme="xcode", ng-ace-file-name="{{leftBar.editorFileName}}", ng-ace-file="{{leftBar.editorFilePath}}" ng-model="aceFileModel") 2 | div(class="prompt-window-options") 3 | span(class="prompt-window-infos", title="{{leftBar.rightClickedProject.path}}") 4 | img(src="img/loading.svg", width="13", ng-show="(savingFile && !savedFile) || loadingFile") 5 | i(class="fa fa-check color-primary", ng-show="savedFile && !savingFile") 6 | | {{leftBar.rightClickedProject.dirName}}/{{leftBar.editorFileName}} 7 | button(ng-click="saveFile()") 8 | | Save 9 | button(ng-click="leftBar.editorFilePath = undefined; aceFileModel = undefined;") 10 | | Close 11 | div(class="window") 12 | div(class="ng-ace-editor", autofocus, ng-autofocus="true") 13 | -------------------------------------------------------------------------------- /lib/scss/progress.scss: -------------------------------------------------------------------------------- 1 | 2 | .left-progress { 3 | font-size: 11px; 4 | line-height: 0; 5 | width: 80%; 6 | margin: 2px 0 0 25px; 7 | 8 | small { 9 | font-size: 11px; 10 | line-height: 11px; 11 | max-height: 11px; 12 | overflow: hidden; 13 | i { 14 | font-size: 9px; 15 | } 16 | &.running-end { 17 | text-indent: -2px; 18 | display: none; 19 | } 20 | } 21 | 22 | &:hover { 23 | small { 24 | &.running-name { 25 | display: none; 26 | } 27 | &.running-end { 28 | display: inherit; 29 | } 30 | } 31 | } 32 | 33 | .left-progress-loading { 34 | height: 6px; 35 | margin-top: 6px; 36 | background: $bg-progress-secondary; 37 | border-radius: 2px; 38 | animation: loadingStripes 1.3s ease-out infinite; 39 | } 40 | 41 | &.left-progress-minor { 42 | 43 | .left-progress-loading { 44 | background: $bg-progress-minor; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/js/errors.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.errors-handler' 4 | , electron = require('electron') 5 | , {remote} = electron 6 | , dialog = remote.dialog; 7 | 8 | angular.module(moduleName, []) 9 | .service('errorsService', /*@ngInject*/ function ErrorHandler($log) { 10 | 11 | this.handleError = (message, error) => { 12 | 13 | /*if (error && error.toString().includes('EACCES')) { 14 | dialog.showErrorBox(message, `\n\n${error}\n\nThis kind of error usually happen when npm has no granted permissions on your machine.\n\nBe sure to have fixed npm permissions.\n\nCheck this simple tutorial on how to fix them: \nhttps://docs.npmjs.com/getting-started/fixing-npm-permissions.`); 15 | }*/ 16 | $log.error(message, error); 17 | }; 18 | 19 | this.showErrorBox = (message, error) => { 20 | 21 | dialog.showErrorBox(message, error); 22 | }; 23 | }); 24 | 25 | export default moduleName; 26 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue Report 2 | 7 | 8 | #### REQUIRED 9 | OS / OS version: 10 | 11 | ndm version: 12 | 13 | node version: 14 | 15 | npm version: 16 | 17 | node installed via (brew, n, nvm, pkg, other ...): 18 | 19 | #### OPTIONAL 20 | 21 | $ which node: 22 | 23 | $ which npm: 24 | 25 | #### IMPORTANT 26 | 27 | - npm permissions fixed or not-fixed? (see https://docs.npmjs.com/getting-started/fixing-npm-permissions): 28 | 29 | - If you are using npm > v4.1.1 then run `npm doctor` and paste the output here: 30 | 31 | #### APPRECIATED 32 | _open the devtools console: OS Menu -> View -> Developer -> Open DevTools_ 33 | 34 | Now make a screenshot of your devtools console or paste the devtools console full-log in here: 35 | 36 | #### THE PROBLEM 37 | What's the problem you facing, in few lines: 38 | 39 | #### REPRODUCE THE PROBLEM 40 | How to reproduce the problem in few lines: 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /doc/CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | ## :tada: Come Contribute! :tada: 2 | 3 | We'll be much grateful if you help and contribute to the project, in any way, even a feature request. 4 | 5 | Doors are wide open! 6 | 7 | 8 | ### How to contribute 9 | 10 | - Fork this repository 11 | - Open the repository folder in your IDE 12 | - Make your changes to the files you intend to edit 13 | - Commit the changes to your forked repo 14 | - Create a Pull Request 15 | - Done! 16 | 17 | To run the app while developing [is this simple](https://github.com/720kb/ndm/blob/master/doc/DEVELOP.md) 18 | 19 | ### Which programming languages 20 | 21 | Languages you'll need to have experience with in order to contribute to *ndm* are simply: Javascript (ES6) and CSS. 22 | 23 | Tools we use: 24 | 25 | - Angular 26 | - Electron 27 | - node 28 | - gulp 29 | - Sass 30 | - Babel 31 | - npm 32 | - bash 33 | - svg 34 | 35 | ### Guidelines 36 | 37 | Below are some Contribution Guidelines, consider reading these before to contribute, just that! 38 | 39 | [Contributing Guidelines](https://github.com/720kb/ndm/blob/master/CONTRIBUTING.md) 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='UTF-8') 5 | 6 | link(rel='stylesheet', href='icons/fontello/css/fontello-embedded.css', media='screen', charset='utf-8') 7 | link(rel='stylesheet', href='../node_modules/bootstrap/dist/css/bootstrap.min.css', media='screen', charset='utf-8') 8 | link(rel='stylesheet', href='css/index.css', media='screen', charset='utf-8') 9 | 10 | script(type='text/javascript', src='../node_modules/angular/angular.min.js') 11 | script(type='text/javascript', src='../node_modules/selection-model/dist/selection-model.min.js') 12 | script(type='text/javascript', src='../node_modules/ace-builds/src-min-noconflict/ace.js') 13 | 14 | script(type='text/javascript', src='js/index.js') 15 | body(ng-app='ndm', ng-controller='ShellController as shell', ng-drag-drop) 16 | .app-big-loading 17 | img(src="img/loading.svg") 18 | div 19 | | Loading 20 | .page 21 | include ./left.pug 22 | .right-column 23 | include ./content.pug 24 | 25 | include ./footer.pug 26 | -------------------------------------------------------------------------------- /lib/scss/updates.scss: -------------------------------------------------------------------------------- 1 | body.updates { 2 | background: $bg-light; 3 | 4 | * { 5 | color: $color-222; 6 | } 7 | 8 | h1 { 9 | font-size: 14px; 10 | line-height: 16px; 11 | margin-bottom: 0; 12 | } 13 | 14 | h2 { 15 | font-size: 13px; 16 | font-weight: normal; 17 | margin-top: 10px; 18 | line-height: 17px; 19 | 20 | &.is-uptodate, &.is-to-update { 21 | i { 22 | color: $color-positive; 23 | } 24 | } 25 | } 26 | 27 | img { 28 | width: 17px; 29 | position: relative; 30 | top: -1px; 31 | margin-right: 1px; 32 | } 33 | 34 | small { 35 | font-size: 11.5px; 36 | color: $color-muted-less; 37 | } 38 | 39 | .left { 40 | text-align: center; 41 | } 42 | 43 | .logo { 44 | margin-top: 10vh; 45 | width: 90px; 46 | } 47 | 48 | button { 49 | margin: 0; 50 | line-height: 21px; 51 | padding: 0 15px; 52 | font-size: 12.5px; 53 | } 54 | 55 | i { 56 | margin-left: -4px; 57 | &.errored { 58 | color: $color-error; 59 | } 60 | } 61 | 62 | progress { 63 | margin-top: 5px; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/scss/ace-editor.scss: -------------------------------------------------------------------------------- 1 | //Leave the !important to override ace-editor default which is rendered later 2 | .ace_editor { 3 | font-size: 13.5px !important; 4 | } 5 | .ace_tooltip { 6 | background: $bg-error !important; 7 | padding: 1.5px 4px !important; 8 | border: 0 !important; 9 | border-radius: 0 !important; 10 | font-size: 12px !important; 11 | color: white !important; 12 | } 13 | .ace_search { 14 | display: none !important; 15 | } 16 | .ace_gutter { 17 | background: none !important; 18 | } 19 | .ace_gutter-cell { 20 | 21 | &.ace_error { 22 | background: none !important; 23 | &::before { 24 | position: absolute; 25 | float: left; 26 | left: 0; 27 | content: '\e801'; 28 | color: $color-error; 29 | font-family: $font-icon; 30 | font-size: 14.5px; 31 | margin: 1px 5px; 32 | text-shadow: 0 1px rgba(255, 255, 255, .75); 33 | } 34 | } 35 | } 36 | 37 | .ace_fold { 38 | background-color: $bg-muted-more !important; 39 | background-image: none !important; 40 | border-radius: 0 !important; 41 | margin:0 !important; 42 | border: 0 !important; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /conf/tasks/front-end.js: -------------------------------------------------------------------------------- 1 | /*global require,console*/ 2 | const gulp = require('gulp') 3 | , gulpPug = require('gulp-pug') 4 | , plumber = require('gulp-plumber') 5 | , runSequence = require('run-sequence') 6 | , sourcemaps = require('gulp-sourcemaps') 7 | , gulpSass = require('gulp-sass') 8 | , paths = require('../paths.json') 9 | , argv = require('yargs').argv 10 | , platform = argv.platform || 'mac'; 11 | 12 | /*eslint-disable no-console */ 13 | console.info(`Setting app for ${platform}`); 14 | /*eslint-enable*/ 15 | 16 | gulp.task('front-end', done => { 17 | 18 | return runSequence( 19 | 'clean', 20 | ['scss', 'pug'], 21 | done); 22 | }); 23 | 24 | gulp.task('scss', () => { 25 | 26 | return gulp.src(`${paths.lib}scss/${platform}/index.scss`) 27 | .pipe(plumber()) 28 | .pipe(sourcemaps.init()) 29 | .pipe(gulpSass({ 30 | 'outputStyle': 'compressed' 31 | })) 32 | .pipe(sourcemaps.write('.')) 33 | .pipe(gulp.dest(`${paths.tmp}/css`)); 34 | }); 35 | 36 | gulp.task('pug', () => { 37 | 38 | return gulp.src(`${paths.lib}**/*.pug`) 39 | .pipe(gulpPug()) 40 | .pipe(gulp.dest(`${paths.tmp}`)); 41 | }); 42 | -------------------------------------------------------------------------------- /lib/icons/fontello/css/fontello-codes.css: -------------------------------------------------------------------------------- 1 | 2 | .fa-times-circle-o:before { content: '\e800'; } /* '' */ 3 | .fa-attention-circled:before { content: '\e801'; } /* '' */ 4 | .fa-globe:before { content: '\e802'; } /* '' */ 5 | .fa-plus-circle:before { content: '\e803'; } /* '' */ 6 | .fa-remove:before { content: '\e804'; } /* '' */ 7 | .fa-caret-right:before { content: '\e805'; } /* '' */ 8 | .fa-caret-down:before { content: '\e806'; } /* '' */ 9 | .fa-check:before { content: '\e807'; } /* '' */ 10 | .fa-folder-o:before { content: '\e808'; } /* '' */ 11 | .fa-caret-up:before { content: '\e809'; } /* '' */ 12 | .fa-lock:before { content: '\e80a'; } /* '' */ 13 | .fa-disk:before { content: '\f0a0'; } /* '' */ 14 | .fa-sort:before { content: '\f0dc'; } /* '' */ 15 | .fa-sort-down:before { content: '\f0dd'; } /* '' */ 16 | .fa-sort-up:before { content: '\f0de'; } /* '' */ 17 | .fa-doctor:before { content: '\f0f1'; } /* '' */ 18 | .fa-circle:before { content: '\f111'; } /* '' */ 19 | .fa-rocket:before { content: '\f135'; } /* '' */ 20 | .fa-level-up:before { content: '\f148'; } /* '' */ 21 | .fa-database:before { content: '\f1c0'; } /* '' */ 22 | .fa-history:before { content: '\f1da'; } /* '' */ 23 | .fa-at:before { content: '\f1fa'; } /* '' */ -------------------------------------------------------------------------------- /lib/js/notification.js: -------------------------------------------------------------------------------- 1 | /*global require Notification*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.notification' 4 | , electron = require('electron') 5 | , BrowserWindow = electron.remote.BrowserWindow; 6 | 7 | angular.module(moduleName, []) 8 | .service('notificationFactory', /*@ngInject*/ () => { 9 | 10 | const notify = (body, skeepFocus, onClickCallback) => { 11 | if (!BrowserWindow.getFocusedWindow() || skeepFocus) { 12 | 13 | let windows 14 | , notification = new Notification('ndm', { 15 | body, 16 | 'sticky': true 17 | }); 18 | 19 | notification.onclick = () => { 20 | if (!skeepFocus) { 21 | windows = BrowserWindow.getAllWindows(); 22 | if (windows[0] && windows[1]) { 23 | //hide updates window and show main window 24 | windows[0].hide(); 25 | windows[1].show(); 26 | windows[1].focus(); 27 | windows[0].hide(); 28 | } 29 | } 30 | notification = undefined; 31 | 32 | if (onClickCallback) { 33 | return onClickCallback(); 34 | } 35 | }; 36 | } 37 | }; 38 | 39 | return { 40 | notify 41 | }; 42 | }); 43 | 44 | export default moduleName; 45 | -------------------------------------------------------------------------------- /lib/top.pug: -------------------------------------------------------------------------------- 1 | .top-menu(top-menu, top-menu-project-path-id="{{tab}}", ng-class="{'freezed': performingAction}") 2 | include ./install-new-package-version.pug 3 | include ./install-new-package.pug 4 | .row 5 | .col-xs-12 6 | button.button-add-package(title="Add Packages", ng-click="showInstallPrompt = true", ng-class="{'active': showInstallPrompt}") 7 | i.fa.fa-plus-circle 8 | | Add package 9 | span(ng-show='showMenuButtons') 10 | button.button-uninstall(title="Uninstall", ng-click="activeClickedLink('5'); uninstallPackage(currentSelectedPackages)", ng-class="{'active': activeLink === '5'}") 11 | i.fa.fa-remove 12 | | Uninstall 13 | button.button-update(title="Update", ng-click="activeClickedLink('2'); updatePackage(currentSelectedPackages)", ng-class="{'active': activeLink === '2'}") 14 | i.fa.fa-level-up 15 | | Update 16 | button.button-latest(title="Install Latest", ng-click="activeClickedLink('3'); installLatest(currentSelectedPackages)", ng-class="{'active': activeLink === '3'}") 17 | i.fa.fa-rocket 18 | | Latest 19 | button.button-version(title="Install Version", ng-show="currentSelectedPackages.length === 1", ng-click="showSpecificVersionPrompt = true", ng-class="{'active': showSpecificVersionPrompt}") 20 | i.fa.fa-at 21 | | Version 22 | -------------------------------------------------------------------------------- /lib/js/directives/ng-resizable.js: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-resizable'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngResizable', /*@ngInject*/ function ngResizable($window, $document) { 7 | return (scope, element) => { 8 | 9 | let getMaxHeight = () => { 10 | return Number($window.innerHeight - 120); 11 | } 12 | , maxHeight = getMaxHeight() 13 | , minHeight = 250; 14 | 15 | const onMouseMove = event => { 16 | 17 | element.css({ 18 | 'height': `${event.pageY - Number(element[0].offsetTop)}px` 19 | }); 20 | 21 | if (element[0].offsetHeight <= minHeight) { 22 | element.css('height', `${minHeight}px`); 23 | } 24 | 25 | if (element[0].offsetHeight >= maxHeight) { 26 | element.css('height', `${maxHeight}px`); 27 | } 28 | } 29 | , onMouseUp = () => { 30 | $document.unbind('mousemove', onMouseMove); 31 | $document.unbind('mouseup', onMouseUp); 32 | }; 33 | 34 | element.on('mousedown', event => { 35 | event.preventDefault(); 36 | $document.on('mousemove', onMouseMove); 37 | $document.on('mouseup', onMouseUp); 38 | }); 39 | 40 | angular.element($window).on('resize', () => { 41 | maxHeight = getMaxHeight(); 42 | }); 43 | }; 44 | }); 45 | 46 | export default moduleName; 47 | -------------------------------------------------------------------------------- /lib/scss/tabs.scss: -------------------------------------------------------------------------------- 1 | .tab { 2 | overflow-x: hidden; 3 | .tab-menu { 4 | display: flex; 5 | overflow: hidden; 6 | height: 23px; 7 | background: $bg-light; 8 | } 9 | .tab-button { 10 | display: inline-flex; 11 | font-size: 13px; 12 | background: $bg-light; 13 | padding: 4px 3px 5px 6px; 14 | line-height: 15px; 15 | white-space: nowrap; 16 | position: relative; 17 | bottom: -1px; 18 | 19 | img { 20 | vertical-align: top; 21 | margin-right: 3px; 22 | } 23 | a { 24 | opacity: .5; 25 | border-radius: 3px; 26 | color: #666; 27 | margin: 0 4px; 28 | margin-left: 8px; 29 | 30 | i { 31 | display: block; 32 | width: 13px; 33 | } 34 | 35 | &:hover { 36 | opacity: 1; 37 | } 38 | } 39 | &.active { 40 | border-radius: 4px 4px 0 0; 41 | background: white; 42 | 43 | a { 44 | opacity: 1; 45 | font-size: 7px; 46 | position: relative; 47 | bottom: -1px; 48 | background: $color-error; 49 | color: white; 50 | text-shadow: 0 -1px rgba(0, 0, 0, .3); 51 | box-shadow: 0 2px 3px red inset; 52 | right: -2px; 53 | height: 13px; 54 | width: 13px; 55 | line-height: 12px; 56 | text-align: center; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/install-new-package-version.pug: -------------------------------------------------------------------------------- 1 | div.dialog.prompt(ng-show="showSpecificVersionPrompt && currentSelectedPackages.length === 1", ng-init="versionPackageVersion = undefined") 2 | form(ng-submit='installVersionPackage(currentSelectedPackages[0], versionPackageVersion)') 3 | input(placeholder='Package name', 4 | type='text', 5 | readonly, 6 | disabled, 7 | ng-value="currentSelectedPackages[0].name") 8 | = " " 9 | input(class="hide", 10 | placeholder='@version', 11 | ng-autofocus, 12 | type='text', 13 | ng-model='versionPackageVersion', 14 | ng-value="versionPackageVersion") 15 | = " " 16 | span(class="prompt-kind") 17 | select(name="packageVersionSelect", ng-model="pkgVersionModel", ng-change="versionPackageVersion = pkgVersionModel") 18 | option(value="", selected) 19 | | - 20 | option(ng-repeat="pkgVersion in selectedPackageViewInfos.versions | orderBy : pkgVersion : 'reverse' track by $index", ng-value="pkgVersion") 21 | | {{pkgVersion}} 22 | button(ng-disabled="installingPackageVersion") 23 | span(ng-show="!installingPackageVersion") 24 | | Install 25 | span(ng-show="installingPackageVersion") 26 | img(src="img/loading.svg", width="13") 27 | = " " 28 | = " " 29 | button(class="button-close-prompt pull-right", type="button", ng-click="hideInstallVersionPrompt();") 30 | i(class="fa fa-remove") 31 | -------------------------------------------------------------------------------- /lib/scss/header.scss: -------------------------------------------------------------------------------- 1 | .top-menu { 2 | width: calc(100% - 8px); 3 | margin: 0 auto; 4 | background: white; 5 | margin-top: 4px; 6 | margin-bottom: 2px; 7 | 8 | &.freezed { 9 | cursor: wait; 10 | * { 11 | cursor: wait; 12 | pointer-events: none; 13 | } 14 | } 15 | 16 | img { 17 | width: 23px; 18 | margin-top: 4px; 19 | margin-right: 6px; 20 | } 21 | 22 | .button-add-package, 23 | .button-update, 24 | .button-uninstall, 25 | .button-version, 26 | .button-latest { 27 | float: right; 28 | font-size: 12px; 29 | padding-left: 0; 30 | padding-right: 3px; 31 | margin: 1px 0 0 4px; 32 | line-height: 13px; 33 | 34 | i { 35 | color: $color-222; 36 | vertical-align: baseline; 37 | } 38 | 39 | } 40 | 41 | .button-add-package { 42 | float: left; 43 | border: 0; 44 | line-height: 18px; 45 | margin: 0 auto; 46 | padding: 0 0 0 0; 47 | background: none; 48 | margin-bottom: 3px; 49 | border-radius: 10px; 50 | 51 | i { 52 | color: $color-primary; 53 | } 54 | 55 | &:focus { 56 | opacity: .7; 57 | } 58 | } 59 | 60 | .button-uninstall { 61 | 62 | i { 63 | color: $color-error; 64 | } 65 | } 66 | 67 | .button-update { 68 | 69 | i { 70 | color: $color-green; 71 | } 72 | } 73 | 74 | 75 | a { 76 | 77 | &:active, &.active { 78 | i { 79 | color: $color-primary; 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/img/npm-logo-cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /lib/scss/settings.scss: -------------------------------------------------------------------------------- 1 | * { 2 | outline: none; 3 | text-decoration: none; 4 | 5 | &:hover, &:active, &:focus { 6 | outline: none; 7 | text-decoration: none; 8 | } 9 | } 10 | 11 | body, html { 12 | -webkit-user-select: none; 13 | color: $color-222; 14 | background: $bg-light; 15 | font-size: 13px; 16 | height: 100vh; 17 | margin: 0 auto; 18 | overflow: hidden; 19 | padding: 0; 20 | } 21 | 22 | a { 23 | cursor: default; 24 | outline: none; 25 | color: $color-222; 26 | text-decoration: none; 27 | 28 | &:hover, &:active, &:focus { 29 | cursor: default; 30 | outline: none; 31 | text-decoration: none; 32 | color: $color-222; 33 | } 34 | } 35 | 36 | small { 37 | font-size: 12px; 38 | 39 | * { 40 | font-size: 12px; 41 | } 42 | } 43 | 44 | pre { 45 | border: none; 46 | font-size: 11.5px; 47 | line-height: 20px; 48 | } 49 | 50 | button { 51 | 52 | i { 53 | font-size: 13px; 54 | vertical-align: middle; 55 | } 56 | 57 | &[disabled] { 58 | opacity: .5; 59 | } 60 | } 61 | 62 | ::-webkit-input-placeholder { 63 | font-weight: lighter; 64 | } 65 | 66 | input { 67 | 68 | &:focus { 69 | box-shadow: 0 0 4px $color-royalblue; 70 | } 71 | 72 | &[disabled] { 73 | opacity: .5; 74 | } 75 | 76 | &[readonly] { 77 | cursor: not-allowed; 78 | } 79 | } 80 | 81 | h1, h2, h3, h4, h5, h6 { 82 | color: $color-666; 83 | font-weight: bold; 84 | } 85 | 86 | b, strong { 87 | 88 | font-weight: 600; 89 | } 90 | 91 | //RESET BUTTONS DUE BOOTSTRAP STYLE 92 | -------------------------------------------------------------------------------- /doc/RECOMMENDATIONS.md: -------------------------------------------------------------------------------- 1 | ## Recommendations :ok_hand: 2 | 3 | - It is highly recommended to install node and npm via brew or nvm or n or similars 4 | - It is highly recommended (when developing or testing ndm) to not start the app with `sudo` (WRONG! `sudo npm run...`) 5 | - It is highly recommended to not rename `node_modules/` folder in your projects (which is a standard naming for node pkgs folder and should never be renamed) 6 | - It is recommended to manage only versioned projects with ndm (git, svn, mercurial etc..). This way everything can be reverted to it's previous/original status in case of unlikely events that gone wrong. 7 | - It is recommended to install and always use the LTS node version (brew or nvm or n or similars will help you to manage this with comfort) 8 | - It is highly recommended to fix npm permissions on your machine (if not already fixed). This means no more `sudo` for global actions. How to fix permissions is simple and written here: https://docs.npmjs.com/getting-started/fixing-npm-permissions 9 | - It is recommended to always run the latest version of ndm 10 | - It is highly reccomended to not install packages globally if those packages aren't meant/developed to be installed globally. You might face strange problems when trying to uninstall them and probably other related problems. 11 | - It is recomended to not change default npm configs, npm config and the use of .npmrc aren't yet fully supported by ndm (we will remove this recommandation as soon has these features will be implemented/supported) 12 | 13 | 🌈 Happy npm desktop managing! 14 | -------------------------------------------------------------------------------- /lib/footer.pug: -------------------------------------------------------------------------------- 1 | .footer.bg-footer 2 | span(class="badge-version", title="Current npm version", ng-mouseover="shell.updateNpmBadgeVersion()") 3 | b 4 | | npm 5 | = " " 6 | small(ng-if="shell.npmCurrentVersionBadge") 7 | | v{{shell.npmCurrentVersionBadge}} 8 | button.button-global(type="button", title="Enable ndm in global folder", ng-show="shell.globalDisabled", ng-click="shell.enableGlobal()") 9 | i.fa.fa-globe.color-primary 10 | | Enable globals 11 | button.button-update(type="button", ng-show="!shell.globalDisabled && shell.npmCurrentVersionBadge", title="Update npm", ng-click="shell.updateNpm()") 12 | i.fa.fa-history 13 | | Update npm 14 | span(class="npm-status pull-right", ng-mouseenter="shell.checkRegistryStatus()") 15 | i.fa.fa-disk(title="npm registry is available", ng-show="!shell.loadingRegistryStatus && shell.registryStatus") 16 | i.fa.fa-disk(title="npm registry checking ...", ng-show="shell.loadingRegistryStatus") 17 | i.fa.fa-disk(title="npm registry is unavailable", ng-show="!shell.loadingRegistryStatus && !shell.registryStatus") 18 | div.loader(ng-class="{'loading': shell.loadingRegistryStatus}") 19 | i.fa.fa-circle(ng-class="{'available': !shell.loadingRegistryStatus && shell.registryStatus}") 20 | i.fa.fa-circle(ng-class="{'unavailable': !shell.loadingRegistryStatus && !shell.registryStatus}") 21 | span(class="pull-right") 22 | button.button-doctor(type="button", title="Run doctor", ng-click="shell.activeClickedLink('doctor'); shell.runDoctor()") 23 | i.fa.fa-doctor 24 | | Doctor 25 | -------------------------------------------------------------------------------- /lib/scss/variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | fonts 3 | */ 4 | $font-icon: 'fontello'; 5 | /* 6 | colors 7 | */ 8 | $color-error: #e81616; 9 | $color-stripes-one: rgba(55, 255, 255, .4); 10 | $color-stripes-two: rgba(155, 255, 255, .5); 11 | $color-primary: #327dff; 12 | $color-positive: $color-primary; 13 | $color-muted: rgba(0, 0, 0, .4); 14 | $color-muted-more: rgba(0, 0, 0, .28); 15 | $color-muted-less: rgba(0, 0, 0, .5); 16 | $color-royalblue: royalblue; 17 | $color-green: #00d842; 18 | $color-npm: #cc0000; 19 | $color-999: #999; 20 | $color-777: #777; 21 | $color-666: #666; 22 | $color-444: #444; 23 | $color-222: #222; 24 | $color-ddd: #ddd; 25 | $color-ccc: #ccc; 26 | /* 27 | backgrounds 28 | */ 29 | $bg-header: #dedede; 30 | $bg-light: #f0f0f0; 31 | $bg-lighter: #fcfcfc; 32 | $bg-eee: #eee; 33 | $bg-muted: rgba(0, 0, 0, .35); 34 | $bg-muted-more: rgba(0, 0, 0, .15); 35 | $bg-muted-invisible: rgba(0, 0, 0, .065); 36 | $bg-error: #e81616; 37 | $bg-table-row: #eee; 38 | $bg-table-row-highlight: $color-positive; 39 | $bg-table-row-loading: repeating-linear-gradient(90deg, $color-primary, $color-primary 5px, 0px, #0882f5 10px); 40 | $bg-progress: repeating-linear-gradient(135deg, $color-primary, $color-primary 5px, #147ada 5px, #147ada 10px); 41 | $bg-progress-secondary: repeating-linear-gradient(135deg, $color-primary, $color-primary 5px, #147ada 5px, #147ada 10px); 42 | $bg-progress-minor: repeating-linear-gradient(135deg, #d83232, #d83232 5px, #c71212 5px, #c71212 10px);; 43 | $bg-tags: #d7ebff; 44 | /*border-radius*/ 45 | $border-radius-inputs: 3px; 46 | $border-radius-button: 3px; 47 | $border-radius-tags: 2px; 48 | $border-radius-prompt: 0 0 2px 2px; 49 | /*border-color*/ 50 | $border-tags: 1px solid #a9cffd; 51 | -------------------------------------------------------------------------------- /lib/js/loading.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | const moduleName = 'npm-ui.loading'; 3 | 4 | angular.module(moduleName, []) 5 | .service('loadingFactory', /*@ngInject*/ function loadingFactory($document) { 6 | 7 | const bodyElement = $document.find('body') 8 | , appReady = () => { 9 | bodyElement.addClass('ready'); 10 | } 11 | , loading = () => { 12 | // bodyElement.addClass('loading'); 13 | } 14 | , finished = () => { 15 | bodyElement.removeClass('loading'); 16 | } 17 | , freeze = () => { 18 | bodyElement.addClass('freezed'); 19 | } 20 | , unfreeze = () => { 21 | bodyElement.removeClass('freezed'); 22 | }; 23 | 24 | return { 25 | loading, 26 | finished, 27 | freeze, 28 | unfreeze, 29 | appReady 30 | }; 31 | }) 32 | .directive('npmLoading', /*@ngInject*/ $rootScope => { 33 | 34 | return { 35 | 'scope': true, 36 | 'restrict': 'A', 37 | 'templateUrl': 'npm-update-log.html', 38 | 'controller': /*@ngInject*/ function NpmLoadingController($scope) { 39 | 40 | const unregisterOnNpmLogs = $rootScope.$on('npm:log:log', (eventInfo, npmLog) => { 41 | $rootScope.$apply(() => { 42 | if (npmLog.type === 'installLatest' && 43 | npmLog.data) { 44 | $scope.log.logs.push(npmLog.data); 45 | } 46 | }); 47 | }); 48 | 49 | this.logs = []; 50 | 51 | $scope.$on('$destroy', () => { 52 | unregisterOnNpmLogs(); 53 | }); 54 | }, 55 | 'controllerAs': 'log' 56 | }; 57 | }); 58 | 59 | export default moduleName; 60 | -------------------------------------------------------------------------------- /lib/install-new-package.pug: -------------------------------------------------------------------------------- 1 | div.dialog.prompt(ng-show="showInstallPrompt") 2 | form(ng-submit='installPackage(packageName, newPackageKind)') 3 | 4 | div(class="tags-input", ng-tag-input, tab-path-id="{{tab}}" ng-autofocus, ng-model="packageName", ng-keyup="search(packageName[packageName.length - 1].name)", contenteditable="true", ng-attr-disabled="{{installingPackage ? 'disabled' : ''}}" placeholder="package<@version> ...") 5 | 6 | input(ng-hide="true", ng-model="searchKeywords", ng-bind="packageName") 7 | = " " 8 | span(class="prompt-kind") 9 | input(type="checkbox", ng-model="newPackageKind", ng-disabled="tab === ''") 10 | = " " 11 | = " " 12 | | dev 13 | = " " 14 | 15 | button(id="install-new-packages-button", ng-disabled="installingPackage || !packageName") 16 | span(ng-show="!installingPackage") 17 | | Install 18 | span(ng-show="installingPackage") 19 | img(src="img/loading.svg", width="13") 20 | = " " 21 | = " " 22 | button(class="button-close-prompt pull-right", type="button", ng-click="hideInstallPrompt();") 23 | i(class="fa fa-remove") 24 | .prompt-search(ng-hide="installingPackage") 25 | .prompt-search-content 26 | .prompt-search-item(ng-repeat="item in searchResults.objects", ng-click="searchChoosePackage(item.package.name)") 27 | h5 28 | | {{item.package.name}} 29 | div 30 | | {{item.package.description}} 31 | .prompt-search-loader(ng-show="searchingNpm") 32 | img(src="img/loading.svg") 33 | | Loading results ... 34 | -------------------------------------------------------------------------------- /lib/scss/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | -webkit-app-region: drag; 3 | bottom: 3px; 4 | font-weight: normal; 5 | height: 21px; 6 | padding: 0 11px; 7 | position: fixed; 8 | width: 100%; 9 | z-index: 9; 10 | font-size: 12px; 11 | 12 | b { 13 | font-weight: 500; 14 | } 15 | 16 | i { 17 | font-size: 16px; 18 | } 19 | .loader { 20 | width: 7px; 21 | display: inline-block; 22 | line-height: 2px; 23 | 24 | i { 25 | width: 7px; 26 | min-width: 10px; 27 | position: relative; 28 | text-align: center; 29 | font-size: 5px; 30 | top: -1px; 31 | color: $color-muted; 32 | 33 | &.available { 34 | color: $color-green; 35 | } 36 | 37 | &.unavailable { 38 | color: $color-error; 39 | } 40 | } 41 | &.loading { 42 | i { 43 | animation: toggleOpacity .4s linear infinite; 44 | 45 | &:first-child { 46 | animation: toggleOpacityInverse .25s linear infinite; 47 | } 48 | } 49 | } 50 | } 51 | 52 | span { 53 | &.badge-version { 54 | small { 55 | font-size: 10px; 56 | } 57 | i { 58 | font-size: 11px; 59 | color: $color-positive; 60 | } 61 | } 62 | &.npm-status { 63 | 64 | i { 65 | &.checking { 66 | color: $color-muted; 67 | } 68 | &.unavailable { 69 | color: $color-error; 70 | } 71 | } 72 | } 73 | } 74 | 75 | button { 76 | margin-left: 6px; 77 | margin-right: 5px; 78 | font-size: 11.5px; 79 | line-height: 12px; 80 | padding: 0 2px 0 0; 81 | height: 19px; 82 | 83 | i { 84 | color: $color-positive; 85 | position: relative; 86 | top: -1px; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/package-informations.pug: -------------------------------------------------------------------------------- 1 | div.table-infos-content 2 | div.information 3 | b 4 | | Name: 5 | = " " 6 | | {{packageViewInfos.name || '-'}} 7 | div.information(title="{{packageViewInfos.description}}") 8 | b 9 | | Description: 10 | = " " 11 | | {{packageViewInfos.description || '-' | removeHTML}} 12 | div.information(title="Package dependencies") 13 | b 14 | | Dependencies: 15 | = " " 16 | span(ng-repeat="(dep, value) in packageViewInfos.dependencies") 17 | a(title="Open in browser", ng-click="shell.openBrowserLink('https://npmjs.com/package/' + dep)") 18 | | {{ dep }} 19 | span(ng-if="!$last") 20 | | , 21 | = " " 22 | span(ng-if="!packageViewInfos.dependencies || packageViewInfos.dependencies.length <= 0") 23 | | - 24 | div.information(title="Package repository url") 25 | b 26 | | Repository: 27 | = " " 28 | | {{packageViewInfos.repository.url || '-'}} 29 | div.information 30 | b 31 | | Issues: 32 | = " " 33 | a(title="Open in browser", ng-if="packageViewInfos.bugs.url", ng-click="shell.openBrowserLink(packageViewInfos.bugs.url)") 34 | | {{packageViewInfos.bugs.url}} 35 | span(ng-if="!packageViewInfos.bugs || !packageViewInfos.bugs.url") 36 | | - 37 | div.information 38 | b 39 | | Url: 40 | = " " 41 | a(title="Open in browser", ng-init="pkgUrlToNpmJsWebsite = 'https://npmjs.com/package/' + packageViewInfos.name", ng-if="packageViewInfos.name", ng-click="shell.openBrowserLink(pkgUrlToNpmJsWebsite)") 42 | | {{ pkgUrlToNpmJsWebsite }} 43 | span(ng-if="!packageViewInfos.name") 44 | | - 45 | div.information 46 | b 47 | | License: 48 | = " " 49 | | {{packageViewInfos.license.type || packageViewInfos.license || '-'}} 50 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | formatter: stylish 3 | files: 4 | include: '**/*.s+(a|c)ss' 5 | rules: 6 | # Extends 7 | extends-before-mixins: 1 8 | extends-before-declarations: 1 9 | placeholder-in-extend: 1 10 | 11 | # Mixins 12 | mixins-before-declarations: 1 13 | 14 | # Line Spacing 15 | one-declaration-per-line: 1 16 | empty-line-between-blocks: 1 17 | single-line-per-selector: 0 18 | 19 | # Disallows 20 | no-color-keywords: 0 21 | no-color-literals: 0 22 | no-css-comments: 0 23 | no-debug: 1 24 | no-duplicate-properties: 1 25 | no-empty-rulesets: 1 26 | no-extends: 0 27 | no-ids: 3 28 | no-important: 1 29 | no-invalid-hex: 1 30 | no-mergeable-selectors: 1 31 | no-misspelled-properties: 1 32 | no-qualifying-elements: 0 33 | no-trailing-zero: 1 34 | no-transition-all: 1 35 | no-url-protocols: 1 36 | no-vendor-prefixes: 3 37 | no-warn: 1 38 | 39 | # Nesting 40 | force-attribute-nesting: 3 41 | force-element-nesting: 1 42 | force-pseudo-nesting: 1 43 | 44 | # Name Formats 45 | function-name-format: 1 46 | mixin-name-format: 1 47 | placeholder-name-format: 1 48 | variable-name-format: 1 49 | 50 | # Style Guide 51 | border-zero: 0 52 | brace-style: 1 53 | clean-import-paths: 1 54 | empty-args: 1 55 | hex-length: 1 56 | hex-notation: 1 57 | indentation: 1 58 | leading-zero: 1 59 | nesting-depth: 0 60 | property-sort-order: 3 61 | quotes: 1 62 | shorthand-values: 0 63 | url-quotes: 1 64 | variable-for-property: 1 65 | zero-unit: 1 66 | 67 | # Inner Spacing 68 | space-after-comma: 1 69 | space-before-colon: 1 70 | space-after-colon: 1 71 | space-before-brace: 1 72 | space-before-bang: 1 73 | space-after-bang: 1 74 | space-between-parens: 1 75 | 76 | # Final Items 77 | trailing-semicolon: 1 78 | final-newline: 1 79 | -------------------------------------------------------------------------------- /lib/js/npm/npm-runner.js: -------------------------------------------------------------------------------- 1 | /*global require,process,Buffer*/ 2 | import NpmOperations from './npm-operations.js'; 3 | 4 | const npm = require('npm') 5 | , stream = require('stream') 6 | , writable = new stream.Writable({ 7 | 'write': (chunk, encoding, next) => { 8 | const thisLogBuffer = new Buffer(chunk) 9 | , thisLog = thisLogBuffer 10 | .toString() 11 | .trim(); 12 | 13 | if (thisLog) { 14 | 15 | process.send({ 16 | 'type': 'log', 17 | 'payload': thisLog 18 | }); 19 | } 20 | 21 | next(); 22 | } 23 | }) 24 | , npmDefaultConfiguration = { 25 | 'loglevel': 'info', 26 | 'progress': false, 27 | 'logstream': writable 28 | } 29 | , exec = (folder, isGlobal, command, param1, param2) => { 30 | const confObject = Object.assign({}, 31 | npmDefaultConfiguration, 32 | { 33 | 'prefix': folder, 34 | 'global': isGlobal 35 | }); 36 | 37 | process.send({folder, isGlobal, command, param1, param2}); 38 | return npm.load(confObject, (err, configuredNpm) => { 39 | if (err) { 40 | 41 | process.send({ 42 | 'type': 'error', 43 | 'payload': err 44 | }); 45 | } 46 | const npmOperations = new NpmOperations(folder, configuredNpm, isGlobal); 47 | 48 | npmOperations[command](param1, param2).then(resolved => process.send({ 49 | 'type': command, 50 | 'payload': resolved 51 | })); 52 | }); 53 | } 54 | , inputs = process.argv 55 | .slice(2) 56 | .map(element => { 57 | try { 58 | 59 | return JSON.parse(element); 60 | } catch (err) { 61 | 62 | if (element === 'undefined') { 63 | 64 | return undefined; 65 | } 66 | 67 | return element; 68 | } 69 | }); 70 | 71 | exec(...inputs); 72 | -------------------------------------------------------------------------------- /conf/tasks/es6-build.js: -------------------------------------------------------------------------------- 1 | /*global require*/ 2 | const gulp = require('gulp') 3 | , rollup = require('rollup').rollup 4 | , rollupJSON = require('rollup-plugin-json') 5 | , rollupBabel = require('rollup-plugin-babel') 6 | , runSequence = require('run-sequence') 7 | , paths = require('../paths.json'); 8 | 9 | gulp.task('es6-build', done => { 10 | 11 | return runSequence( 12 | 'front-end', 13 | 'ndm', 14 | 'ndm-updater', 15 | 'npm-runner', 16 | done); 17 | }); 18 | 19 | gulp.task('npm-runner', () => { 20 | 21 | return rollup({ 22 | 'entry': `${paths.lib}js/npm/npm-runner.js`, 23 | 'plugins': [ 24 | rollupJSON(), 25 | rollupBabel({ 26 | 'presets': [ 27 | 'es2015-rollup' 28 | ] 29 | }) 30 | ] 31 | }).then(bundle => { 32 | 33 | return bundle.write({ 34 | 'format': 'iife', 35 | 'moduleId': 'npm-ui-ng', 36 | 'moduleName': 'npm-ui-ng', 37 | 'sourceMap': true, 38 | 'dest': `${paths.tmp}/npm-runner.js` 39 | }); 40 | }); 41 | }); 42 | 43 | gulp.task('ndm', () => { 44 | 45 | return rollup({ 46 | 'entry': `${paths.lib}js/index.js`, 47 | 'plugins': [ 48 | rollupJSON(), 49 | rollupBabel({ 50 | 'presets': [ 51 | 'es2015-rollup' 52 | ] 53 | }) 54 | ] 55 | }).then(bundle => { 56 | 57 | return bundle.write({ 58 | 'format': 'iife', 59 | 'moduleId': 'npm-ui-ng', 60 | 'moduleName': 'npm-ui-ng', 61 | 'sourceMap': true, 62 | 'dest': `${paths.tmp}/js/index.js` 63 | }); 64 | }); 65 | }); 66 | 67 | gulp.task('ndm-updater', () => { 68 | 69 | return rollup({ 70 | 'entry': `${paths.lib}js/update.js`, 71 | 'plugins': [ 72 | rollupJSON(), 73 | rollupBabel({ 74 | 'presets': [ 75 | 'es2015-rollup' 76 | ] 77 | }) 78 | ] 79 | }).then(bundle => { 80 | 81 | return bundle.write({ 82 | 'format': 'iife', 83 | 'moduleId': 'npm-updater-ng', 84 | 'moduleName': 'npm-updater-ng', 85 | 'sourceMap': true, 86 | 'dest': `${paths.tmp}/js/update.js` 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /lib/icons/fontello/css/animation.css: -------------------------------------------------------------------------------- 1 | /* 2 | Animation example, for spinners 3 | */ 4 | .animate-spin { 5 | -moz-animation: spin 2s infinite linear; 6 | -o-animation: spin 2s infinite linear; 7 | -webkit-animation: spin 2s infinite linear; 8 | animation: spin 2s infinite linear; 9 | display: inline-block; 10 | } 11 | @-moz-keyframes spin { 12 | 0% { 13 | -moz-transform: rotate(0deg); 14 | -o-transform: rotate(0deg); 15 | -webkit-transform: rotate(0deg); 16 | transform: rotate(0deg); 17 | } 18 | 19 | 100% { 20 | -moz-transform: rotate(359deg); 21 | -o-transform: rotate(359deg); 22 | -webkit-transform: rotate(359deg); 23 | transform: rotate(359deg); 24 | } 25 | } 26 | @-webkit-keyframes spin { 27 | 0% { 28 | -moz-transform: rotate(0deg); 29 | -o-transform: rotate(0deg); 30 | -webkit-transform: rotate(0deg); 31 | transform: rotate(0deg); 32 | } 33 | 34 | 100% { 35 | -moz-transform: rotate(359deg); 36 | -o-transform: rotate(359deg); 37 | -webkit-transform: rotate(359deg); 38 | transform: rotate(359deg); 39 | } 40 | } 41 | @-o-keyframes spin { 42 | 0% { 43 | -moz-transform: rotate(0deg); 44 | -o-transform: rotate(0deg); 45 | -webkit-transform: rotate(0deg); 46 | transform: rotate(0deg); 47 | } 48 | 49 | 100% { 50 | -moz-transform: rotate(359deg); 51 | -o-transform: rotate(359deg); 52 | -webkit-transform: rotate(359deg); 53 | transform: rotate(359deg); 54 | } 55 | } 56 | @-ms-keyframes spin { 57 | 0% { 58 | -moz-transform: rotate(0deg); 59 | -o-transform: rotate(0deg); 60 | -webkit-transform: rotate(0deg); 61 | transform: rotate(0deg); 62 | } 63 | 64 | 100% { 65 | -moz-transform: rotate(359deg); 66 | -o-transform: rotate(359deg); 67 | -webkit-transform: rotate(359deg); 68 | transform: rotate(359deg); 69 | } 70 | } 71 | @keyframes spin { 72 | 0% { 73 | -moz-transform: rotate(0deg); 74 | -o-transform: rotate(0deg); 75 | -webkit-transform: rotate(0deg); 76 | transform: rotate(0deg); 77 | } 78 | 79 | 100% { 80 | -moz-transform: rotate(359deg); 81 | -o-transform: rotate(359deg); 82 | -webkit-transform: rotate(359deg); 83 | transform: rotate(359deg); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/js/assets.js: -------------------------------------------------------------------------------- 1 | /*global require,console*/ 2 | import angular from 'angular'; 3 | 4 | const moduleName = 'npm-ui.assets' 5 | , fs = require('fs') 6 | , path = require('path') 7 | , storage = require('electron-storage'); 8 | 9 | angular.module(moduleName, []) 10 | .provider('assets', /*@ngInject*/ function Session() { 11 | const projectsFolder = 'projects.json' 12 | , projects = []; 13 | 14 | storage.get(projectsFolder) 15 | .then(data => { 16 | 17 | if (data && 18 | data.length) { 19 | 20 | data.forEach(item => { 21 | let isPath 22 | , isShrinkwrap; 23 | 24 | if (item && 25 | item.path) { 26 | 27 | try { 28 | //is a directory? 29 | if (fs.lstatSync(item.path).isDirectory()) { 30 | isPath = true; 31 | } else { 32 | isPath = false; 33 | } 34 | } catch (excp) { 35 | isPath = false; 36 | console.warn(`Unable to read project path: ${excp}`); 37 | } 38 | 39 | try { 40 | //is shrinkwrapped -> has npm-shrinkwrap.json inside? 41 | if (fs.existsSync(path.join(item.path, 'npm-shrinkwrap.json'))) { 42 | isShrinkwrap = true; 43 | } else { 44 | isShrinkwrap = false; 45 | } 46 | } catch (excp) { 47 | isShrinkwrap = false; 48 | console.warn(`No npm-shrinkwrap.json found in project path: ${excp}`); 49 | } 50 | 51 | if (isShrinkwrap) { 52 | item.shrinkwrap = true; 53 | } else { 54 | item.shrinkwrap = false; 55 | } 56 | 57 | if (isPath) { 58 | projects.push(item); 59 | } 60 | } 61 | }); 62 | } 63 | }) 64 | .catch(err => () => { 65 | console.err(`Unable to retrieve saved projects: ${err}`); 66 | }); 67 | 68 | projects.save = projectInfo => storage.set(projectsFolder, projectInfo); 69 | 70 | this.$get = /*@ngInject*/ () => ({ 71 | projects 72 | }); 73 | }); 74 | 75 | export default moduleName; 76 | -------------------------------------------------------------------------------- /lib/icons/fontello/css/fontello-ie7-codes.css: -------------------------------------------------------------------------------- 1 | 2 | .fa-times-circle-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 3 | .fa-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 4 | .fa-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 5 | .fa-plus-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 6 | .fa-remove { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 7 | .fa-caret-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 8 | .fa-caret-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 9 | .fa-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 10 | .fa-folder-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 11 | .fa-caret-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 12 | .fa-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 13 | .fa-disk { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 14 | .fa-sort { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 15 | .fa-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 16 | .fa-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 17 | .fa-doctor { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 18 | .fa-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 19 | .fa-rocket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 20 | .fa-level-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 21 | .fa-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 22 | .fa-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 23 | .fa-at { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -------------------------------------------------------------------------------- /lib/history-prompt.pug: -------------------------------------------------------------------------------- 1 | div.dialog.dialog-window(ng-if="leftBar.showHistoryPrompt", ng-init="leftBar.showSnapshotStatus = []; leftBar.selectedSnapshot = undefined;") 2 | div(class="prompt-window-options") 3 | span(class="prompt-window-infos", title="{{leftBar.rightClickedProject.path}}") 4 | img(src="img/loading.svg", width="13", ng-show="leftBar.restoringSnapshot[leftBar.rightClickedProject.path] && !leftBar.restoredSnapshot[leftBar.rightClickedProject.path]") 5 | = " " 6 | | {{leftBar.rightClickedProject.dirName}}/ 7 | button(ng-click="leftBar.showHistoryPrompt = undefined;") 8 | | Close 9 | button(ng-click="leftBar.deleteSnapshot()", ng-if="leftBar.selectedSnapshot && leftBar.projectHistory && leftBar.projectHistory.length > 0") 10 | | Delete 11 | button(ng-if="leftBar.projectHistory && leftBar.projectHistory.length > 0 && leftBar.selectedSnapshot", ng-click="leftBar.restoreSnapshot()", ng-disabled="leftBar.restoringSnapshot[leftBar.rightClickedProject.path]") 12 | span(ng-show="!leftBar.restoringSnapshot[leftBar.rightClickedProject.path]") 13 | | Restore 14 | span(ng-show="leftBar.restoringSnapshot[leftBar.rightClickedProject.path]") 15 | | Restoring 16 | div(class="window") 17 | div(class="prompt-window-holder", ng-if="!leftBar.projectHistory || leftBar.projectHistory && leftBar.projectHistory.length <= 0") 18 | | You have no snapshots for this project. 19 | div(class="prompt-history-item", ng-class="{'active': leftBar.selectedSnapshot === item}", ng-repeat="item in leftBar.projectHistory | orderBy : $index : -1", ng-click="leftBar.selectedSnapshot = item;") 20 | div 21 | a(ng-click="leftBar.showSnapshotStatus[item.datetime] = true;", ng-show="!leftBar.showSnapshotStatus[item.datetime]") 22 | i(class="fa fa-caret-right color-black") 23 | a(ng-click="leftBar.showSnapshotStatus[item.datetime] = false;", ng-show="leftBar.showSnapshotStatus[item.datetime]") 24 | i(class="fa fa-caret-down color-black") 25 | i(class="fa fa-database color-primary") 26 | = " " 27 | | {{item.datetime}} 28 | div(class="prompt-history-status", ng-if="leftBar.showSnapshotStatus[item.datetime]", ng-ace-editor, ng-ace-editor-readonly="true" ng-ace-editor-mode="json", ng-ace-editor-theme="xcode", ng-model="aceFileModel", ng-ace-source="{{item.status}}") 29 | div(class="ng-ace-editor") 30 | -------------------------------------------------------------------------------- /lib/scss/layout.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | height: calc(100vh - 23px); 3 | } 4 | 5 | .right-column { 6 | border-radius: 2px; 7 | float: left; 8 | margin: 10px 0; 9 | width: calc(100vw - 200px); 10 | height: calc(100vh - 37px); 11 | background: white; 12 | overflow: hidden; 13 | } 14 | 15 | .left-column { 16 | border-radius: 2px; 17 | float: left; 18 | height: calc(100vh - 37px); 19 | overflow: auto; 20 | margin: 10px 5px 10px 10px; 21 | padding: 0 15px 15px 15px; 22 | width: 175px; 23 | background: white; 24 | border: 1px solid $color-ccc; 25 | 26 | a { 27 | display: inline-block; 28 | margin: 0 auto; 29 | padding: 0 6px; 30 | white-space: nowrap; 31 | min-width: 100%; 32 | line-height: 20px; 33 | 34 | &:not(.project) { 35 | line-height: 25px; 36 | } 37 | 38 | img { 39 | &.global-img { 40 | width: 20px; 41 | margin-right: 3px; 42 | margin-top: -3px; 43 | } 44 | } 45 | 46 | i { 47 | font-size: 14.5px; 48 | margin-right: 2px; 49 | margin-left: 7px; 50 | color: $color-primary; 51 | } 52 | 53 | &.shrinkwrapped { 54 | 55 | i { 56 | &.fa-lock { 57 | color: $color-444; 58 | margin:0 auto; 59 | margin-left: -6px; 60 | margin-right: -2px; 61 | font-size: 11.5px; 62 | } 63 | 64 | &.fa-folder-o { 65 | color: $color-npm; 66 | } 67 | } 68 | } 69 | 70 | b { 71 | font-weight: normal; 72 | white-space: nowrap; 73 | width: 100%; 74 | } 75 | 76 | &:active, &:focus { 77 | background: $color-ddd; 78 | } 79 | 80 | &.project { 81 | 82 | &:hover { 83 | 84 | span.button-delete-project { 85 | display: inherit; 86 | } 87 | } 88 | } 89 | 90 | span.button-delete-project { 91 | display: none; 92 | width: 26px; 93 | line-height: 21px; 94 | height: 20px; 95 | text-align: center; 96 | position: absolute; 97 | left: 157.5px; 98 | 99 | i { 100 | color: $color-222; 101 | margin: 0 auto; 102 | font-size: 13px; 103 | } 104 | } 105 | } 106 | 107 | h6 { 108 | font-weight: 500; 109 | font-size: 12px; 110 | margin-bottom: 5px; 111 | padding: 5px 9px; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/update.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='UTF-8') 5 | 6 | link(rel='stylesheet', href='icons/fontello/css/fontello-embedded.css', media='screen', charset='utf-8') 7 | link(rel='stylesheet', href='../node_modules/bootstrap/dist/css/bootstrap.min.css', media='screen', charset='utf-8') 8 | link(rel='stylesheet', href='css/index.css', media='screen', charset='utf-8') 9 | 10 | script(type='text/javascript', src='../node_modules/angular/angular.min.js') 11 | script(type='text/javascript', src='../node_modules/ace-builds/src-min-noconflict/ace.js') 12 | 13 | script(type='text/javascript', src='js/update.js') 14 | body.updates(ng-app='ndm-updater') 15 | .container(ng-controller="ShellController as vm") 16 | .col-xs-4.left 17 | img.logo(src="../icon.ico") 18 | .copy 19 | small 20 | | © 720kb 21 | .col-xs-8 22 | h1 23 | | ndm 24 | div 25 | small 26 | | Installed version {{ vm.currentVersion }} 27 | 28 | h2(ng-show="vm.checking") 29 | img(src="img/loading.svg") 30 | | Checking for updates ... 31 | h2(ng-show="vm.errorChecking") 32 | i(class="fa fa-attention-circled errored") 33 | | Unavailable please retry later. 34 | 35 | h2.is-uptodate(ng-show="!vm.checking && vm.toUpdate === false") 36 | i(class="fa fa-check") 37 | | Already up to date 38 | div 39 | small 40 | | ndm is up to date with {{ vm.currentVersion }} 41 | 42 | button(ng-click="vm.checkNow()", ng-show="!vm.updating && !vm.checking && !vm.toUpdate") 43 | | Check again 44 | 45 | h2.is-to-update(ng-show="vm.toUpdate") 46 | | Updates available 47 | div 48 | small 49 | | Install the updates now. 50 | button(ng-click="vm.updateIt()", ng-show="vm.toUpdate") 51 | | Install and Relaunch 52 | 53 | h2.is-downloading(ng-show="vm.updating") 54 | span(ng-show="!vm.progress || vm.progress <= 90") 55 | | Downloading updates ... 56 | span(ng-show="vm.progress > 90") 57 | | Installing updates ... 58 | progress(class="col-xs-11", ng-value="vm.progress", max="100") 59 | 60 | h2(ng-show="vm.errorUpdating") 61 | i(class="fa fa-attention-circled errored") 62 | | Error updating, please retry later. 63 | div 64 | small 65 | | This is weird... consider download ndm again. 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*global require,process,__dirname*/ 2 | const {app, Menu, BrowserWindow, shell} = require('electron') 3 | , path = require('path') 4 | , url = require('url') 5 | , packageJSON = require('./package.json') 6 | , applicationTemplate = packageJSON.appTemplate; 7 | 8 | //Set main window height bigger for Windows ONLY 9 | if (process.platform === 'win32') { 10 | applicationTemplate.minHeight += 30; 11 | applicationTemplate.height += 30; 12 | } 13 | //Set main window height smaller for Linux ONLY 14 | if (process.platform !== 'win32' && 15 | process.platform !== 'darwin') { 16 | applicationTemplate.minHeight -= 20; 17 | applicationTemplate.height -= 20; 18 | } 19 | app.on('window-all-closed', () => { 20 | app.quit(); 21 | }); 22 | 23 | app.on('ready', () => { 24 | 25 | const mainWindow = new BrowserWindow(applicationTemplate) 26 | , updateWindow = new BrowserWindow({ 27 | 'width': 400, 28 | 'height': 192, 29 | 'parent': mainWindow, 30 | 'show': false, 31 | 'resizable': false, 32 | 'maximizable': false, 33 | 'alwaysOnTop': true, 34 | 'fullscreenable': false, 35 | 'title': '' 36 | }) 37 | , OSMenu = require('./menu.js')(mainWindow, updateWindow, shell, packageJSON, app); 38 | 39 | Menu.setApplicationMenu(Menu.buildFromTemplate(OSMenu)); 40 | updateWindow.loadURL(url.format({ 41 | 'pathname': path.resolve(__dirname, 'dist', 'update.html'), 42 | 'protocol': 'file:', 43 | 'slashes': true 44 | })); 45 | 46 | updateWindow.on('close', event => { 47 | event.preventDefault(); 48 | 49 | mainWindow.webContents.send('loading:unfreeze-app'); 50 | updateWindow.hide(); 51 | }); 52 | 53 | mainWindow.on('ready-to-show', () => { 54 | 55 | mainWindow.show(); 56 | }); 57 | 58 | mainWindow.on('page-title-updated', event => { 59 | //lock app title otherwise gets the index.html filename 60 | event.preventDefault(); 61 | }); 62 | 63 | mainWindow.on('restore', () => { 64 | //hide autoupdates window 65 | updateWindow.hide(); 66 | mainWindow.webContents.send('loading:unfreeze-app'); 67 | }); 68 | 69 | mainWindow.on('enter-full-screen', () => { 70 | //hide autoupdates window 71 | updateWindow.hide(); 72 | mainWindow.webContents.send('loading:unfreeze-app'); 73 | }); 74 | 75 | mainWindow.on('closed', () => { 76 | 77 | app.quit(); 78 | }); 79 | 80 | mainWindow.loadURL(url.format({ 81 | 'pathname': path.resolve(__dirname, 'dist', 'index.html'), 82 | 'protocol': 'file:', 83 | 'slashes': true 84 | })); 85 | }); 86 | -------------------------------------------------------------------------------- /lib/js/directives/ng-table-keyboard.js: -------------------------------------------------------------------------------- 1 | /*global*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-table-keyboard'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngTableKeyboard', /*@ngInject*/ function ngTableKeyboard($window, $document) { 7 | return (scope, element) => { 8 | 9 | const onArrowDown = () => { 10 | let tableRows = element[0].querySelectorAll('.tab:not(.ng-hide) .table-row:not(.disabled)') 11 | , clickedElement; 12 | 13 | if (tableRows && tableRows.length > 0) { 14 | tableRows.forEach((row, index) => { 15 | if (angular.element(row).hasClass('selected')) { 16 | if (!clickedElement) { 17 | if (tableRows[index + 1]) { 18 | clickedElement = true; 19 | angular.element(tableRows[index + 1]).triggerHandler('click'); 20 | } 21 | } 22 | } 23 | }); 24 | if (!clickedElement) { 25 | angular.element(tableRows[0]).triggerHandler('click'); 26 | } 27 | } 28 | } 29 | , onArrowUp = () => { 30 | let tableRows = element[0].querySelectorAll('.table-row:not(.disabled)') 31 | , clickedElement; 32 | 33 | if (tableRows && tableRows.length > 0) { 34 | tableRows.forEach((row, index) => { 35 | if (angular.element(row).hasClass('selected')) { 36 | if (!clickedElement) { 37 | if (tableRows[index - 1]) { 38 | clickedElement = true; 39 | angular.element(tableRows[index - 1]).triggerHandler('click'); 40 | return false; 41 | } 42 | } 43 | } 44 | }); 45 | if (!clickedElement) { 46 | angular.element(tableRows[0]).triggerHandler('click'); 47 | } 48 | } 49 | } 50 | , bindOnKey = event => { 51 | //if not loading 52 | if (!angular.element($document[0].body).hasClass('loading')) { 53 | if (event && 54 | event.keyCode) { 55 | if (event.keyCode === 40) { 56 | onArrowDown(); 57 | } 58 | if (event.keyCode === 38) { 59 | onArrowUp(); 60 | } 61 | } 62 | } 63 | }; 64 | 65 | angular.element($window).bind('keydown', bindOnKey); 66 | scope.$on('destroy', () => { 67 | angular.element($window).unbind('keydown', bindOnKey); 68 | }); 69 | }; 70 | }); 71 | 72 | export default moduleName; 73 | -------------------------------------------------------------------------------- /lib/icons/fontello/css/fontello-ie7.css: -------------------------------------------------------------------------------- 1 | [class^="fa-"], [class*=" fa-"] { 2 | font-family: 'fontello'; 3 | font-style: normal; 4 | font-weight: normal; 5 | 6 | /* fix buttons height */ 7 | line-height: 1em; 8 | 9 | /* you can be more comfortable with increased icons size */ 10 | /* font-size: 120%; */ 11 | } 12 | 13 | .fa-times-circle-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 14 | .fa-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 15 | .fa-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 16 | .fa-plus-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 17 | .fa-remove { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 18 | .fa-caret-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 19 | .fa-caret-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 20 | .fa-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 21 | .fa-folder-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 22 | .fa-caret-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 23 | .fa-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 24 | .fa-disk { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 25 | .fa-sort { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 26 | .fa-sort-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 27 | .fa-sort-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 28 | .fa-doctor { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 29 | .fa-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 30 | .fa-rocket { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 31 | .fa-level-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 32 | .fa-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 33 | .fa-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 34 | .fa-at { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -------------------------------------------------------------------------------- /doc/FAQ.md: -------------------------------------------------------------------------------- 1 | ## FAQ 2 | 3 | **1) I use the CLI, why use an app?** 4 | 5 | Of course, we all love the npm CLI, no jokes. 6 | 7 | It is obviously very powerful. 8 | 9 | *ndm* is an alternative experience to the `npmCLI` and here are all the pros: 10 | 11 | - Less struggling with long terminal logs, scrolling to find eventual warnings and/or errors. 12 | - All your projects are on the same view, no more multiple `cd` to move from a project to another, just drag them all into the app view. 13 | - Notifications (specially when your long long npm install finishes) 14 | - List all your npm global and local packages in one maybe two clicks. 15 | - Cleaner view of all your npm projects and dependencies. 16 | - Run npm commands and scripts in one maybe two clicks. 17 | - Search npm packages and see packages infos like a pro 18 | - More features to be finished and to come ... 19 | 20 | Some of the greatest and widely used package managers got their own GUI... brew, apt ... just for example. 21 | npm got none. That's bad, that doesn't help npm itself and the npm community. 22 | 23 | Here is ndm, give it a try before to say "_no, mmmmm, no, meh, maybe_" :ok_hand: 24 | 25 | **Obviously:** using **ndm** doesn't mean you can no longer use the CLI. 26 | 27 | **2) Is ndm stable?** 28 | 29 | The first releases are not guaranteed to be very stable, some problem/bug may happen. 30 | 31 | Just give it some time, have some patience and if you like ndm project then come contribute! 32 | Your help is always appreciated and you are welcome anytime! 33 | 34 | 35 | **3) Do i have to worry about anything when using ndm?** 36 | 37 | Actually not, not really. 38 | **ndm** does not run any malicious or env/system breaking commands in background, and it doesn't run anything outside of the npm native commands. 39 | 40 | If you want to be 100% sure about it, just look at the source code, which is clear and very readable. 41 | 42 | Then (if you have 5 minutes to spend) what we suggest is to read this tiny mini guide of sane [recommendations](https://github.com/720kb/ndm/blob/master/doc/RECOMMENDATIONS.md). 43 | 44 | **4) Why is ndm so slow on my pc?** 45 | 46 | **ndm** speed depends exclusively on your pc/device specs and [npm-cli](https://docs.npmjs.com/cli/npm) speed. 47 | We can't do much to speed up your machine or the npm native commands. 48 | 49 | **5) Yarn?** 50 | 51 | _Premise: **ndm** was born several months before Yarn was out._ 52 | 53 | Yarn is a great tool, we are looking forward to seeing what happens: both on the Yarn and the npm side. 54 | 55 | Many things could change in the meantime. 56 | 57 | That said: if you have any idea or suggestion you are welcome to share and discuss! 58 | 59 | **6) Support?** 60 | 61 | Just open an issue, we'll be in touch :ok_hand: 62 | -------------------------------------------------------------------------------- /lib/js/interface/top.js: -------------------------------------------------------------------------------- 1 | /*global */ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.top-menu'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('topMenu', /*@ngInject*/ function TopMenuController($document, $rootScope, $log, $timeout, npm) { 7 | return (scope, element, attrs) => { 8 | 9 | let searchTimeout //debounce search 10 | , prevSearchKeyword; 11 | 12 | const topMenuIdentifierPath = attrs.topMenuProjectPathId; 13 | 14 | scope.destroyActiveClickedLink = () => { 15 | scope.activeLink = undefined; 16 | }; 17 | 18 | scope.activeClickedLink = activeLink => { 19 | if ((activeLink === '1' || activeLink === '4') && 20 | scope.activeLink === activeLink) { 21 | //toggle prompts show/hide 22 | scope.activeLink = false; 23 | } else { 24 | 25 | scope.activeLink = activeLink; 26 | $rootScope.$emit('top-bar:active-link', { 27 | 'link': activeLink 28 | }); 29 | } 30 | }; 31 | 32 | scope.search = keyword => { 33 | $log.info('search', keyword); 34 | if (keyword && 35 | keyword.trim() !== prevSearchKeyword) { 36 | /*eslint-disable*/ 37 | if (searchTimeout) { 38 | $timeout.cancel(searchTimeout); 39 | } 40 | prevSearchKeyword = keyword; 41 | /*eslint-enable*/ 42 | searchTimeout = $timeout(() => { 43 | scope.searchingNpm = true; 44 | scope.searchResults = []; 45 | npm.npmInFolder(topMenuIdentifierPath).then(npmInFolder => { 46 | npmInFolder.search(keyword).then(data => { 47 | scope.$apply(() => { 48 | scope.searchingNpm = false; 49 | scope.searchResults = data; 50 | }); 51 | }).catch(err => { 52 | scope.$apply(() => { 53 | scope.searchingNpm = false; 54 | scope.searchResults = []; 55 | }); 56 | $log.error('SEARCH ERROR', err); 57 | }); 58 | }); 59 | }, 500); 60 | } else { 61 | scope.searchingNpm = false; 62 | scope.searchResults = []; 63 | } 64 | }; 65 | 66 | scope.searchChoosePackage = pkgName => { 67 | //update digits in input 68 | scope.$evalAsync(() => { 69 | scope.packageName[scope.packageName.length - 1].name = pkgName; 70 | scope.searchResults = []; 71 | $log.warn(pkgName, scope.packageName); 72 | //communicate to ng-tag-input to update itself and model 73 | $rootScope.$emit('top-menu:search-choosen-package', {'data': scope.packageName, 'tabPath': topMenuIdentifierPath}); 74 | }); 75 | }; 76 | 77 | scope.hideInstallPrompt = () => { 78 | scope.showInstallPrompt = false; 79 | }; 80 | 81 | scope.hideInstallVersionPrompt = () => { 82 | scope.showSpecificVersionPrompt = false; 83 | }; 84 | }; 85 | }); 86 | 87 | export default moduleName; 88 | -------------------------------------------------------------------------------- /lib/img/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | If you are so kind to help and support us, please consider following these guidelines before sending PRs or commits: 4 | 5 | - Possibly no lint errors (eslint, jscs, scss, pug etc ...) if accidentally you find some then feel free to fix it as you go. 6 | 7 | - Possibly no formatting errors (if you use [Atom](https://atom.io/) you just have to use the IDE default settings for formatting code and you are synced) 8 | 9 | - Possibly use ONLY English language (everywhere in the code and outside the code) 10 | 11 | - Possibly no English typos (it can happen of course, just try to avoid them as much as possible) 12 | 13 | - Possibly no comments inside the code 14 | 15 | - Possibly, just use .lint files in your IDE (don't remove or disable the linters: .eslintrc, .jscslint and so on) 16 | 17 | - Possibly, just do not disable linters with inline comments (or at least remove comments as you want to PR) but be sure there are no errors in the end. 18 | 19 | - If you are editing the GUI style (CSS) do not change or edit .pug/.html files to fit your style; CSS must fit the html structure and not the opposite. 20 | 21 | - As you finished to code your changes always make a new clean npm install (`rm -Rf node_modules/ && npm install`) 22 | , Then run the app and test all your changes very deeply (`npm start`) 23 | 24 | - Be sure to always update your node version to LTS before to start coding 25 | 26 | - If you are not sure or you have any doubt about what you are doing/editing: consider opening an issue and ask, before to go PR or commit. You can even join the [live chat](https://gitter.im/720kb/ndm) and ask there. 27 | 28 | - If your changes are radicals, please open an issue or contact us [here](https://gitter.im/720kb/ndm), so that we can discuss it togheter before everything goes on. By radicals we can list for example: 29 | - changed the HTML layout 30 | - changed UI and UX behaviors 31 | - changed logo or icons or graphics in general 32 | - changed package.json by changing | updating | removing dependencies 33 | - added new js files to the folder structure 34 | - changed the project folders structure 35 | - rewrote js file/s for a good 50% and up 36 | 37 | These guidelines are not imperative at all, it's just the simplest method we have to be synced with you. 38 | You can PR any file in the repo: even this same file you are now reading. :ok_hand: 39 | 40 | #### Look! One thing... 41 | 42 | To be absolutely clear with you: 43 | 44 | contributing on the ***ndm*** project, and in general on open source projects, does not mean to get paid or receive any good for the time/ and work and service you freely provide for the project. It is your time/service/work and you provide it on your own choice; Contributing to **ndm** doesn't mean neither to get hired or to get a job in any company. By reading these contribution guidelines and before to contribute on the **ndm** project you declare you have read and accepted these conditions, thank you. 45 | 46 | Thank you for listening :speaker:! We really would love and hope to have you on board, to have some fun and share new tricks and tips, each others of course! 47 | 48 | Bests. 49 | -------------------------------------------------------------------------------- /lib/icons/fontello/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with ') 52 | .run(/*@ngInject*/ function RunInitStorage(appHistoryFile, $log) { 53 | //create storage file in case 54 | Storage.isPathExists(appHistoryFile, exist => { 55 | if (exist) { 56 | $log.info('Storage: OK'); 57 | } else { 58 | Storage.set(appHistoryFile, '{}', err => { 59 | if (err) { 60 | $log.error('Not able to initialize storage for the app'); 61 | } else { 62 | $log.info('Storage initialized for the app'); 63 | } 64 | }); 65 | } 66 | }); 67 | }) 68 | .run(/*@ngInject*/ function onLoadingEvents(loadingFactory) { 69 | ipcRenderer.on('loading:freeze-app', () => { 70 | loadingFactory.freeze(); 71 | }); 72 | 73 | ipcRenderer.on('loading:unfreeze-app', () => { 74 | loadingFactory.unfreeze(); 75 | }); 76 | }) 77 | .run(/*@ngInject*/ function RunOnlineOfflineCheck($window, notificationFactory) { 78 | //alert user when he goes offLine 79 | const showMessageAlert = () => { 80 | notificationFactory.notify('You are offline. ndm may not work as expected.', true); 81 | } 82 | , onOffline = () => { 83 | showMessageAlert(); 84 | } 85 | , onStart = () => { 86 | if (navigator && 87 | !navigator.onLine) { 88 | showMessageAlert(); 89 | } 90 | }; 91 | 92 | angular.element($window).on('offline', onOffline); 93 | onStart(); 94 | }) 95 | .run(/*ngInject*/ function runDomReady($document, $rootScope, $timeout, $log, loadingFactory, timeoutForWhenUserIsPresent) { 96 | $document.ready(() => { 97 | $log.info('DOM is ready'); 98 | //communicate to the app DOM is ready 99 | $rootScope.$emit('dom:ready'); 100 | $timeout(() => { 101 | //ga user is on 102 | try { 103 | visitor.pageview(`/platform/${process.platform}`).send(); 104 | $log.info(`Platform ${process.platform}`); 105 | } catch (excp) { 106 | $log.warn('Unable to send ga pageview', excp); 107 | } 108 | }, timeoutForWhenUserIsPresent); 109 | }); 110 | }) 111 | .run(/*ngInject*/ function runNpmReady($rootScope, $log, loadingFactory) { 112 | $rootScope.$on('npm:ready', () => { 113 | $log.info('npm is ready'); 114 | loadingFactory.appReady(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /lib/content.pug: -------------------------------------------------------------------------------- 1 | .content(ng-controller="ContentController as content") 2 | span(npm-loading) 3 | include ./npm-doctor-log.pug 4 | .row.home.bg-ultralight(ng-show="content.tabs.length <= 0") 5 | div 6 | .separator10 7 | small.color-black 8 | | Select or drag npm projects to start 9 | .separator10 10 | .separator10 11 | button.home-button(ng-click="shell.openChooser()") 12 | | Add projects 13 | .tab(npm-tabs, ng-repeat="tab in content.tabs", npm-tab-id="{{tab}}", ng-show="content.activeTab === tab && tab") 14 | .tab-menu 15 | span.tab-button(ng-repeat="tab in content.tabs", ng-class="{'active': content.activeTab === tab}", ng-click="content.activeTab = tab") 16 | spanner(ng-if="tab === ''") 17 | img.global-img(src="img/npm-logo-cube.svg") 18 | | Globals 19 | spanner(ng-if="tab !== ''") 20 | | {{ tab | lastNameInPath}} 21 | a(ng-click="content.closeProjectTab(tab)") 22 | i(class="fa fa-remove") 23 | include ./top.pug 24 | div(ng-show="tab") 25 | .row.table-header 26 | .col-xs-4.clickable(ng-click="sortTableBy('name')") 27 | | Package 28 | i(class="fa", ng-class="{'fa-sort': !tableOrderBy.includes('-name') || !tableOrderBy.includes('name'), 'fa-sort-down': tableOrderBy.includes('-name'), 'fa-sort-up': tableOrderBy.includes('name')}") 29 | .col-xs-2 30 | | Current 31 | .col-xs-2 32 | | Wanted 33 | .col-xs-2 34 | | Latest 35 | .col-xs-2.clickable(ng-click="sortTableBy('kind')") 36 | | Env 37 | i(class="fa", ng-class="{'fa-sort': !tableOrderBy.includes('-kind') || !tableOrderBy.includes('kind'), 'fa-sort-down': tableOrderBy.includes('-kind'), 'fa-sort-up': tableOrderBy.includes('kind')}") 38 | .table-body(ng-table-keyboard, ng-class="{'freezed': showLoadingSelectedRow}") 39 | .table-loader(ng-show="loading && !loaded") 40 | .table-loader-content 41 | img(src='img/loading.svg') 42 | | Loading packages... 43 | 44 | .row.table-row.disabled(ng-repeat='aPackage in packageInformations', ng-if="isGlobalProject && aPackage.name === 'npm'", title="Do not perform npm global actions from here") 45 | .col-xs-4 {{ aPackage.name }} 46 | .col-xs-2 47 | span(ng-class="{'color-positive font-light': !aPackage.wanted && !aPackage.latest}") 48 | | {{ aPackage.current }} 49 | .col-xs-2 50 | i(class="fa fa-check", ng-if="!aPackage.wanted && !aPackage.latest") 51 | | {{ aPackage.wanted }} 52 | .col-xs-2 53 | b(ng-if="aPackage.latest") 54 | | {{ aPackage.latest }} 55 | i(class="fa fa-check color-positive", ng-if="!aPackage.wanted && !aPackage.latest") 56 | .col-xs-2 57 | | {{ aPackage.kind }} 58 | 59 | .row.table-row(ng-repeat='aPackage in packageInformations | orderBy: tableOrderBy', ng-hide="!packageInformations", id="table-item-{{$index}}", ng-table-keyboard-selected-items="selectedPackages", ng-if="!isGlobalProject || isGlobalProject && aPackage.name !== 'npm'", selection-model, selection-model-mode="'multiple'", selection-model-selected-items="selectedPackages", ng-click="selectPackages(selectedPackages)", ng-class="{'active': selectedPackages.includes(aPackage), 'table-row-loading': currentSelectedPackages.includes(aPackage) && showLoadingSelectedRow}") 60 | .col-xs-4 {{ aPackage.name }} 61 | .col-xs-2 62 | span(ng-class="{'color-positive font-light': !aPackage.wanted && !aPackage.latest}") 63 | | {{ aPackage.current }} 64 | .col-xs-2 65 | i(class="fa fa-check", ng-if="!aPackage.wanted && !aPackage.latest") 66 | | {{ aPackage.wanted }} 67 | .col-xs-2 68 | b(ng-if="aPackage.latest") 69 | | {{ aPackage.latest }} 70 | i(class="fa fa-check color-positive", ng-if="!aPackage.wanted && !aPackage.latest") 71 | .col-xs-2 72 | | {{ aPackage.kind }} 73 | div 74 | div.table-infos 75 | include ./package-informations.pug 76 | -------------------------------------------------------------------------------- /lib/js/directives/ng-ace-editor.js: -------------------------------------------------------------------------------- 1 | /*global require window*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-ace-editor' 4 | , fs = require('fs') 5 | , ace = window.ace; 6 | 7 | angular.module(moduleName, []) 8 | .directive('ngAceEditor', /*@ngInject*/ function ngAceEditor($rootScope, $document) { 9 | return { 10 | 'require': '?ngModel', 11 | 'link': (scope, element, attrs, ngModel) => { 12 | 13 | const editorElement = element[0].querySelector('.ng-ace-editor') 14 | , aceEditor = ace.edit(editorElement) 15 | , aceSession = aceEditor.getSession() 16 | , theme = attrs.ngAceEditorTheme 17 | , readonly = scope.$eval(attrs.ngAceEditorReadonly) 18 | , setAceMode = () => { 19 | if (attrs.ngAceFileName.endsWith('.json')) { 20 | 21 | aceSession.setMode('ace/mode/json'); 22 | } else if (attrs.ngAceFileName.startsWith('.')) { 23 | aceSession.setMode('ace/mode/text'); 24 | } 25 | } 26 | , unregisterSavedFile = $rootScope.$on('ace-editor:saved-file', () => { 27 | scope.$evalAsync(() => { 28 | scope.savingFile = false; 29 | scope.savedFile = true; 30 | }); 31 | }) 32 | , unregisterSavingFile = $rootScope.$on('ace-editor:saving-file', () => { 33 | scope.$evalAsync(() => { 34 | scope.savingFile = true; 35 | scope.savedFile = false; 36 | }); 37 | }) 38 | , unregisterLoadedFile = $rootScope.$on('ace-editor:loaded-file', () => { 39 | scope.$evalAsync(() => { 40 | scope.loadingFile = false; 41 | aceEditor.focus(); 42 | }); 43 | }) 44 | , unregisterLoadingFile = $rootScope.$on('ace-editor:loading-file', () => { 45 | scope.$evalAsync(() => { 46 | scope.loadingFile = true; 47 | scope.savedFile = false; 48 | scope.savingFile = false; 49 | setAceMode(); 50 | }); 51 | }); 52 | 53 | attrs.$observe('ngAceFile', filePath => { 54 | if (filePath) { 55 | $rootScope.$emit('ace-editor:loading-file', { 56 | 'path': filePath 57 | }); 58 | try { 59 | if (fs.existsSync(filePath)) { 60 | scope.aceFileModel = fs.readFileSync(filePath).toString(); 61 | } else { 62 | scope.aceFileModel = ''; 63 | aceEditor.setValue(''); 64 | } 65 | } catch (e) { 66 | scope.aceFileModel = ''; 67 | aceEditor.setValue(''); 68 | } 69 | 70 | $rootScope.$emit('ace-editor:loaded-file', { 71 | 'path': filePath, 72 | 'content': scope.aceFileModel 73 | }); 74 | } 75 | }); 76 | 77 | attrs.$observe('ngAceSource', source => { 78 | if (source) { 79 | scope.aceFileModel = source; 80 | } else { 81 | scope.aceFileModel = ''; 82 | } 83 | }); 84 | 85 | scope.saveFile = () => { 86 | $rootScope.$emit('ace-editor:saving-file', { 87 | 'path': attrs.ngAceFile, 88 | 'content': scope.aceFileModel 89 | }); 90 | fs.writeFileSync(attrs.ngAceFile, scope.aceFileModel, {'flag': 'w'}, 'utf8'); 91 | $rootScope.$emit('ace-editor:saved-file', { 92 | 'path': attrs.ngAceFile, 93 | 'content': scope.aceFileModel 94 | }); 95 | }; 96 | 97 | scope.$watch(() => { 98 | return [editorElement.offsetWidth, editorElement.offsetHeight]; 99 | }, () => { 100 | aceEditor.resize(); 101 | aceEditor.setOptions({ 102 | 'showInvisibles': true, 103 | 'cursorStyle': 'smooth', 104 | 'highlightSelectedWord': true, 105 | 'theme': `ace/theme/${theme}`, 106 | 'readOnly': readonly 107 | }); 108 | aceEditor.renderer.updateFull(); 109 | }, true); 110 | 111 | aceSession.on('change', () => { 112 | if (aceSession.getValue()) { 113 | ngModel.$setViewValue(aceSession.getValue()); 114 | } 115 | }); 116 | 117 | ngModel.$render = () => { 118 | if (ngModel.$viewValue) { 119 | aceSession.setValue(ngModel.$viewValue); 120 | } 121 | }; 122 | 123 | $document.on('mousedown mouseup mouseover', () => { 124 | aceEditor.resize(); 125 | }); 126 | 127 | element.on('$destroy', () => { 128 | aceSession.$stopWorker(); 129 | aceEditor.destroy(); 130 | unregisterSavingFile(); 131 | unregisterSavedFile(); 132 | unregisterLoadingFile(); 133 | unregisterLoadedFile(); 134 | }); 135 | } 136 | }; 137 | }); 138 | 139 | export default moduleName; 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ndm", 3 | "version": "1.2.0", 4 | "description": "npm desktop manager", 5 | "main": "index.js", 6 | "homepage": "https://720kb.github.io/ndm", 7 | "github": "https://github.com/720kb/ndm", 8 | "donate": { 9 | "opencollective": "https://opencollective.com/ndm" 10 | }, 11 | "author": { 12 | "name": "720kb", 13 | "email": "tech@720kb.net", 14 | "url": "http://720kb.net" 15 | }, 16 | "authors": [ 17 | "720kb", 18 | "Filippo Oretti", 19 | "Dario Andrei" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/720kb/ndm.git" 24 | }, 25 | "copyright": "©720kb, Filippo Oretti, Dario Andrei", 26 | "bugs": { 27 | "url": "https://github.com/720kb/ndm/issues" 28 | }, 29 | "license": { 30 | "type": "GPL-3.0", 31 | "url": "https://github.com/720kb/ndm/blob/master/LICENSE.md" 32 | }, 33 | "build": { 34 | "appId": "net.720kb.ndm", 35 | "copyright": "© 720kb - Filippo Oretti - Dario Andrei", 36 | "asar": false, 37 | "productName": "ndm", 38 | "icon": "icon.icns", 39 | "mac": { 40 | "category": "public.app-category.productivity", 41 | "target": [ 42 | "dmg", 43 | "zip" 44 | ] 45 | }, 46 | "dmg": { 47 | "backgroundColor": "#cdcdcd" 48 | }, 49 | "linux": { 50 | "maintainer": "720kb.net", 51 | "category": "Utility", 52 | "description": "npm desktop manager", 53 | "packageCategory": "Utility", 54 | "target": [ 55 | "deb", 56 | "rpm", 57 | "zip" 58 | ] 59 | }, 60 | "win": { 61 | "icon": "icon.ico", 62 | "target": [ 63 | "zip", 64 | "nsis" 65 | ] 66 | }, 67 | "files": [ 68 | "node_modules", 69 | "dist", 70 | "index.js", 71 | "menu.js", 72 | "icon.ico", 73 | "LICENSE.md" 74 | ], 75 | "directories": { 76 | "output": "./releases" 77 | } 78 | }, 79 | "social": { 80 | "twitter": { 81 | "url": "https://twitter.com/720kb_" 82 | }, 83 | "gitter": { 84 | "url": "https://gitter.im/720kb/ndm" 85 | } 86 | }, 87 | "appTemplate": { 88 | "title": "ndm", 89 | "width": 640, 90 | "height": 420, 91 | "minWidth": 640, 92 | "minHeight": 420, 93 | "show": false, 94 | "center": true, 95 | "movable": true, 96 | "resizable": true, 97 | "minimizable": true, 98 | "maximizable": true, 99 | "closable": true, 100 | "fullscreenable": true 101 | }, 102 | "scripts": { 103 | "precommit": "npm run lint", 104 | "lint": "gulp lint", 105 | "prestart": "npm install && gulp dist --platform=mac", 106 | "start": "electron .", 107 | "mac": "LANG=en_US.UTF-8 && gulp dist --platform=mac && electron .", 108 | "linux": "gulp dist --platform=linux && electron .", 109 | "win": "gulp dist --platform=win && electron .", 110 | "build": "npm run build-mac && npm run build-linux && npm run build-win", 111 | "build-mac": "gulp distify --platform=mac && build --mac --publish=never && echo 'IMPORTANT! if build fails see https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build , you probably miss some library on your OS, just install them and retry :).'", 112 | "build-win": "gulp distify --platform=win && build --win --publish=never && echo 'IMPORTANT! if build fails see https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build , you probably miss some library on your OS, just install them and retry :).'", 113 | "build-linux": "gulp distify --platform=linux && build --linux --publish=never && echo 'IMPORTANT! if build fails see https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build , you probably miss some library on your OS, just install them and retry :).'", 114 | "build-linux-deb": "gulp distify --platform=linux && build --linux deb --publish=never && echo 'IMPORTANT! if build fails see https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build , you probably miss some library on your OS, just install them and retry :).'", 115 | "postversion": "git push && git push --tags" 116 | }, 117 | "dependencies": { 118 | "ace-builds": "^1.2.5", 119 | "adm-zip": "^0.4.7", 120 | "angular": "^1.6.0", 121 | "bootstrap": "^3.3.6", 122 | "electron-storage": "^1.0.6", 123 | "fs-extra": "^2.0.0", 124 | "node-fetch": "^1.6.3", 125 | "npm": "^4.4.0", 126 | "rimraf": "^2.6.1", 127 | "selection-model": "^0.11.0", 128 | "shell-path": "^2.0.0", 129 | "universal-analytics": "^0.4.8", 130 | "uuid": "^3.0.1" 131 | }, 132 | "devDependencies": { 133 | "babel-preset-es2015-rollup": "^3.0.0", 134 | "del": "^2.2.0", 135 | "electron": "^1.6.6", 136 | "electron-builder": "^17.3.1", 137 | "eslint": "^3.13.1", 138 | "gulp": "^3.9.1", 139 | "gulp-clean-css": "^2.2.0", 140 | "gulp-eslint": "^3.0.1", 141 | "gulp-ng-annotate": "^2.0.0", 142 | "gulp-plumber": "^1.1.0", 143 | "gulp-pug": "^3.2.0", 144 | "gulp-sass": "^3.1.0", 145 | "gulp-sourcemaps": "^2.3.1", 146 | "gulp-uglify": "^2.0.0", 147 | "husky": "^0.12.0", 148 | "pug": "^2.0.0-beta6", 149 | "require-dir": "^0.3.1", 150 | "rollup": "^0.41.1", 151 | "rollup-plugin-babel": "^2.4.0", 152 | "rollup-plugin-json": "^2.0.0", 153 | "run-sequence": "^1.1.5", 154 | "yargs": "^6.5.0" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/js/update.js: -------------------------------------------------------------------------------- 1 | /*globals require,__dirname,process,window,Buffer*/ 2 | import angular from 'angular'; 3 | 4 | const path = require('path') 5 | , {remote} = require('electron') 6 | , fs = require('fs') 7 | , AdmZip = require('adm-zip') 8 | , fse = require('fs-extra') 9 | , tmpDirectory = require('os').tmpdir() 10 | , tmpFolder = path.resolve(tmpDirectory, 'ndm-') 11 | , packageJSON = require(path.resolve(__dirname, '..', 'package.json')); 12 | 13 | angular.module('ndm-updater', []) 14 | .constant('updateUrl', 'https://api.github.com/repos/720kb/ndm/releases/latest') 15 | .controller('ShellController', /* @ngInject */function ShellController( 16 | $scope, 17 | $document, 18 | $log, 19 | updateUrl 20 | ) { 21 | const onUpdateCheckEnd = event => { 22 | 23 | let updateMetaInfo 24 | , indexOfZipName; 25 | 26 | if (process.platform === 'darwin') { 27 | indexOfZipName = 'mac.zip'; 28 | } else if (process.platform === 'win32') { 29 | indexOfZipName = 'win.zip'; 30 | } 31 | 32 | $scope.$apply(() => { 33 | 34 | this.checking = false; 35 | if (!event.target.responseText) { 36 | 37 | this.error = 'No response'; 38 | this.errorChecking = true; 39 | return; 40 | } 41 | 42 | try { 43 | 44 | updateMetaInfo = JSON.parse(event.target.responseText); 45 | $log.info('Github Fetched Metas', updateMetaInfo); 46 | } catch (parsingError) { 47 | 48 | this.error = parsingError; 49 | this.errorChecking = true; 50 | return; 51 | } 52 | 53 | if (updateMetaInfo.tag_name === `v${packageJSON.version}` || 54 | updateMetaInfo.tag_name.indexOf('v') === -1) { 55 | 56 | //Nothing to update 57 | this.toUpdate = false; 58 | return; 59 | } 60 | 61 | //There is something to update 62 | this.nextVesion = updateMetaInfo.name.replace('v', ''); 63 | this.toUpdate = true; 64 | this.url = updateMetaInfo.assets 65 | .filter(element => element.name.indexOf(indexOfZipName) > -1) 66 | .reduce(prev => prev).browser_download_url; 67 | }); 68 | } 69 | , onUpdateCheckErr = () => { 70 | 71 | $scope.$apply(() => { 72 | 73 | this.checking = false; 74 | this.errorChecking = true; 75 | }); 76 | } 77 | , onUpdateErr = () => { 78 | 79 | $scope.$apply(() => { 80 | 81 | this.checking = false; 82 | this.errorUpdating = true; 83 | this.progress = 100; 84 | }); 85 | } 86 | , onUpdateEnd = event => { 87 | 88 | fs.mkdtemp(tmpFolder, (err, folder) => { 89 | const fileToSave = path.resolve(folder, `npm_${new Date().getTime()}`); 90 | let newResources; 91 | 92 | if (process.platform === 'darwin') { 93 | newResources = path.resolve(folder, path.join('ndm.app', 'Contents', 'Resources', 'app')); 94 | } else if (process.platform === 'win32') { 95 | newResources = path.resolve(folder, path.join('win-ia32-unpacked', 'resources', 'app')); 96 | } 97 | 98 | if (err) { 99 | 100 | throw err; 101 | } 102 | 103 | fs.appendFile(fileToSave, new Buffer(event.target.response), appendErr => { 104 | 105 | if (appendErr) { 106 | 107 | throw appendErr; 108 | } 109 | try { 110 | const zip = new AdmZip(fileToSave); 111 | 112 | zip.extractAllTo(/*target path*/folder, /*overwrite*/true); 113 | } catch (excp) { 114 | $log.warn(excp); 115 | } 116 | 117 | fse.move(newResources, remote.app.getAppPath(), { 118 | 'overwrite': true 119 | }, moveErr => { 120 | 121 | if (moveErr) { 122 | 123 | throw moveErr; 124 | } 125 | remote.app.relaunch(); 126 | remote.app.exit(0); 127 | }); 128 | }); 129 | }); 130 | } 131 | , onUpdateProgress = event => { 132 | 133 | $scope.$apply(() => { 134 | 135 | if (event.lengthComputable) { 136 | this.progress = (event.loaded / event.total) * 100; 137 | } else { 138 | 139 | this.progress = (this.progress + 10) % 100; 140 | } 141 | }); 142 | }; 143 | 144 | this.checkNow = () => { 145 | if (!this.checking) { 146 | 147 | this.errorChecking = false; 148 | this.checking = true; 149 | const updateRequest = new window.XMLHttpRequest(); 150 | 151 | delete this.toUpdate; 152 | updateRequest.addEventListener('load', onUpdateCheckEnd); 153 | updateRequest.addEventListener('error', onUpdateCheckErr); 154 | updateRequest.open('GET', updateUrl); 155 | updateRequest.send(); 156 | } 157 | }; 158 | 159 | this.updateIt = () => { 160 | if (!this.updating) { 161 | 162 | this.errorUpdating = false; 163 | this.updating = true; 164 | const updating = new window.XMLHttpRequest(); 165 | 166 | delete this.toUpdate; 167 | updating.addEventListener('load', onUpdateEnd); 168 | updating.addEventListener('error', onUpdateErr); 169 | updating.addEventListener('progress', onUpdateProgress); 170 | 171 | updating.open('GET', this.url); 172 | updating.responseType = 'arraybuffer'; 173 | updating.send(); 174 | } 175 | }; 176 | 177 | this.currentVersion = packageJSON.version; 178 | this.checkNow(); 179 | 180 | $document[0].addEventListener('visibilitychange', () => { 181 | if ($document[0].visibilityState === 'visible' && 182 | !this.checking && 183 | !this.updating) { 184 | this.checkNow(); 185 | } 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /menu.js: -------------------------------------------------------------------------------- 1 | /*global module process*/ 2 | (function withNode() { 3 | 4 | module.exports = (mainWindow, updateWindow, shell, packageJSON, app) => { 5 | 6 | let menuTemplate; 7 | 8 | const aboutMenuItem = { 9 | 'submenu': [ 10 | { 11 | 'role': 'about' 12 | }, 13 | { 14 | 'label': `Version ${packageJSON.version}`, 15 | 'enabled': false 16 | }, 17 | { 18 | 'label': 'Check for Updates...', 19 | click() { 20 | 21 | mainWindow.webContents.send('loading:freeze-app'); 22 | updateWindow.setMenu(null); 23 | updateWindow.show(); 24 | } 25 | }, 26 | { 27 | 'type': 'separator' 28 | }, 29 | { 30 | 'label': 'Visit Website', 31 | click() { 32 | shell.openExternal(packageJSON.homepage); 33 | } 34 | } 35 | ] 36 | } 37 | , fileMenuItem = { 38 | 'label': 'File', 39 | 'submenu': [ 40 | { 41 | 'label': 'Add Project...', 42 | 'accelerator': 'CmdOrCtrl+O', 43 | click() { 44 | mainWindow.webContents.send('menu:add-project-folder'); 45 | } 46 | } 47 | ] 48 | } 49 | , editMenuItem = { 50 | 'label': 'Edit', 51 | 'submenu': [ 52 | { 53 | 'role': 'undo' 54 | }, 55 | { 56 | 'role': 'redo' 57 | }, 58 | { 59 | 'type': 'separator' 60 | }, 61 | { 62 | 'role': 'cut' 63 | }, 64 | { 65 | 'role': 'copy' 66 | }, 67 | { 68 | 'role': 'paste' 69 | }, 70 | { 71 | 'role': 'pasteandmatchstyle' 72 | }, 73 | { 74 | 'role': 'delete' 75 | }, 76 | { 77 | 'role': 'selectall' 78 | } 79 | ] 80 | } 81 | , viewMenuItem = { 82 | 'label': 'View', 83 | 'submenu': [ 84 | { 85 | 'role': 'togglefullscreen' 86 | }, 87 | { 88 | 'type': 'separator' 89 | }, 90 | { 91 | 'label': 'Developer', 92 | 'submenu': [{ 93 | 'label': 'Open DevTools', 94 | click(item, focusedWindow) { 95 | if (focusedWindow) { 96 | focusedWindow.openDevTools(); 97 | } 98 | } 99 | }] 100 | } 101 | ] 102 | } 103 | , windowMenuItem = { 104 | 'role': 'window', 105 | 'submenu': [ 106 | { 107 | 'role': 'minimize' 108 | }, 109 | { 110 | 'role': 'close' 111 | } 112 | ] 113 | } 114 | , helpMenuItem = { 115 | 'role': 'help', 116 | 'submenu': [ 117 | { 118 | 'label': 'More About', 119 | click() { 120 | shell.openExternal(`${packageJSON.github}`); 121 | } 122 | }, 123 | { 124 | 'label': 'Report an issue', 125 | click() { 126 | shell.openExternal(`${packageJSON.bugs.url}`); 127 | } 128 | }, 129 | { 130 | 'type': 'separator' 131 | }, 132 | { 133 | 'label': 'Donate', 134 | click() { 135 | shell.openExternal(packageJSON.donate.opencollective); 136 | } 137 | }, 138 | { 139 | 'type': 'separator' 140 | }, 141 | { 142 | 'label': 'Join Chat', 143 | click() { 144 | shell.openExternal(`${packageJSON.social.gitter.url}`); 145 | } 146 | }, 147 | { 148 | 'label': 'Follow on Twitter', 149 | click() { 150 | shell.openExternal(`${packageJSON.social.twitter.url}`); 151 | } 152 | } 153 | ] 154 | }; 155 | 156 | if (process.platform !== 'darwin' && 157 | process.platform !== 'win32') { 158 | 159 | //if linux no need for "check for updates" 160 | aboutMenuItem.submenu.splice(2, 1); 161 | } 162 | 163 | if (process.platform && 164 | process.platform === 'darwin') { 165 | aboutMenuItem.label = packageJSON.name; 166 | aboutMenuItem.submenu.push({ 167 | 'type': 'separator' 168 | }); 169 | aboutMenuItem.submenu.push({ 170 | 'role': 'hide' 171 | }); 172 | aboutMenuItem.submenu.push({ 173 | 'role': 'hideothers' 174 | }); 175 | aboutMenuItem.submenu.push({ 176 | 'role': 'unhide' 177 | }); 178 | aboutMenuItem.submenu.push({ 179 | 'type': 'separator' 180 | }); 181 | aboutMenuItem.submenu.push({ 182 | 'label': 'Restart', 183 | 'accelerator': 'CmdOrCtrl+R', 184 | click() { 185 | app.relaunch(); 186 | app.quit(); 187 | } 188 | }); 189 | aboutMenuItem.submenu.push({ 190 | 'role': 'quit' 191 | }); 192 | 193 | menuTemplate = [ 194 | aboutMenuItem, 195 | fileMenuItem, 196 | editMenuItem, 197 | viewMenuItem, 198 | windowMenuItem, 199 | helpMenuItem 200 | ]; 201 | } else { 202 | aboutMenuItem.label = 'About'; 203 | 204 | viewMenuItem.submenu.unshift({ 205 | 'label': 'Toggle menu', 206 | click() { 207 | mainWindow.setAutoHideMenuBar(true); 208 | if (mainWindow.isMenuBarVisible()) { 209 | 210 | mainWindow.setMenuBarVisibility(false); 211 | } else { 212 | 213 | mainWindow.setMenuBarVisibility(true); 214 | } 215 | } 216 | }); 217 | 218 | menuTemplate = [ 219 | fileMenuItem, 220 | editMenuItem, 221 | viewMenuItem, 222 | helpMenuItem, 223 | aboutMenuItem 224 | ]; 225 | } 226 | 227 | return menuTemplate; 228 | }; 229 | }()); 230 | -------------------------------------------------------------------------------- /lib/js/directives/ng-tag-input.js: -------------------------------------------------------------------------------- 1 | /*global document window*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-ui.ng-tag-input'; 4 | 5 | angular.module(moduleName, []) 6 | .directive('ngTagInput', /*@ngInject*/ function ngTagInput($rootScope, $log) { 7 | return { 8 | 'require': '?ngModel', 9 | 'link': (scope, element, attrs, ngModel) => { 10 | let ngTagInputIdentifier = attrs.tabPathId 11 | , documentRange 12 | , windowSelection 13 | , focusTheEnd = () => { 14 | try { 15 | element[0].focus(); 16 | documentRange = document.createRange(); 17 | documentRange.selectNodeContents(element[0].lastChild); 18 | documentRange.collapse(false); 19 | windowSelection = window.getSelection(); 20 | windowSelection.removeAllRanges(); 21 | windowSelection.addRange(documentRange); 22 | } catch (excp) { 23 | $log.warn('ng-tag-input warning when setting focus', excp); 24 | } 25 | } 26 | , createTags = () => { 27 | try { 28 | let digits = element[0].innerText.split(' ') 29 | , tags = []; 30 | 31 | if (digits && 32 | digits.length > 0) { 33 | digits.forEach(tag => { 34 | if (tag.trim().length > 0) { 35 | tags.push(`${tag.trim()}`); 36 | } else { 37 | tags.push(tag); 38 | } 39 | }); 40 | if (tags && 41 | tags.length > 0) { 42 | element.html(`${tags.join(' ')}  `); 43 | focusTheEnd(); 44 | } 45 | } 46 | } catch (excp) { 47 | $log.warn('ng-tag-input warning', excp); 48 | } 49 | } 50 | , updateModel = () => { 51 | 52 | let packages = element[0].innerText.trim().split(' ') 53 | , modelValue = [] 54 | , pkgName 55 | , pkgVersion; 56 | 57 | packages.forEach(item => { 58 | if (item.trim() && 59 | item.trim().length > 0) { 60 | 61 | if (item.includes('@')) { 62 | pkgName = item.split('@')[0].replace('@', '').trim(); 63 | pkgVersion = item.split('@')[1].replace('@', '').trim(); 64 | } else { 65 | pkgName = item.trim(); 66 | pkgVersion = false; 67 | } 68 | modelValue.push({ 69 | 'name': pkgName, 70 | 'version': pkgVersion 71 | }); 72 | } 73 | }); 74 | ngModel.$setViewValue(modelValue); 75 | } 76 | , onBlur = () => { 77 | updateModel(); 78 | } 79 | , onKeyUp = event => { 80 | updateModel(); 81 | if (element[0].innerText.trim().length > 0 && 82 | ((event.keyCode && event.keyCode === 32) || 83 | (event.which && event.which === 32)) 84 | ) { 85 | createTags(); 86 | updateModel(); 87 | } 88 | } 89 | , onKeyDown = event => { 90 | updateModel(); 91 | if (event && 92 | event.keyCode && 93 | event.keyCode.toString() === '13' && 94 | element[0].innerText.trim().length > 0) { //enter key to submit form 95 | try { 96 | createTags(); 97 | updateModel(); 98 | //find button to submit form 99 | angular.element(document.querySelector('#install-new-packages-button'))[0].click(); 100 | } catch (excp) { 101 | $log.warn('Cannot find form to submit', excp); 102 | } 103 | } 104 | } 105 | , onKeyPress = () => { 106 | updateModel(); 107 | } 108 | , onPaste = () => { 109 | scope.$evalAsync(() => { 110 | createTags(); 111 | updateModel(); 112 | }); 113 | } 114 | , onTrigger = () => { 115 | focusTheEnd(); 116 | } 117 | , onKeypressDisabled = event => { 118 | return event.preventDefault(); 119 | } 120 | , updateOnSearchChoosenPackage = $rootScope.$on('top-menu:search-choosen-package', (eventInformation, data) => { 121 | if (data && 122 | data.data && 123 | data.tabPath && 124 | data.tabPath === ngTagInputIdentifier) { //if search input is showing on this specific tab 125 | 126 | let newInputValue = ''; 127 | 128 | data.data.forEach(pkg => { 129 | newInputValue += pkg.name; 130 | if (pkg.version && 131 | pkg.version.length > 0) { 132 | newInputValue += `@${pkg.version}`; 133 | } 134 | newInputValue += ' '; //leave a blank space at the end of the string to split into tags again 135 | }); 136 | 137 | element[0].innerText = newInputValue; 138 | createTags(); 139 | updateModel(); 140 | } 141 | }); 142 | 143 | element.on('mousedown', onTrigger); 144 | element.on('click', onTrigger); 145 | element.on('paste', onPaste); 146 | element.on('blur', onBlur); 147 | element.on('keyup', onKeyUp); 148 | element.on('keydown', onKeyDown); 149 | element.on('keypress', onKeyPress); 150 | //disable input on submit 151 | attrs.$observe('disabled', value => { 152 | if (value === 'disabled') { 153 | element.on('keypress', onKeypressDisabled); 154 | } else { 155 | element.unbind('keypress', onKeypressDisabled); 156 | } 157 | }); 158 | scope.$on('$destroy', () => { 159 | updateOnSearchChoosenPackage(); 160 | element.unbind('keyup', onKeyUp); 161 | element.unbind('keydown', onKeyDown); 162 | element.unbind('paste', onPaste); 163 | element.unbind('mousedown', onTrigger); 164 | element.unbind('click', onTrigger); 165 | element.unbind('keypress', onKeyPress); 166 | element.unbind('blur', onBlur); 167 | element.unbind('keypress', onKeypressDisabled); 168 | }); 169 | } 170 | }; 171 | }); 172 | 173 | export default moduleName; 174 | -------------------------------------------------------------------------------- /lib/scss/prompt.scss: -------------------------------------------------------------------------------- 1 | .dialog { 2 | background: $bg-light; 3 | box-shadow: 1px .5px 3px rgba(0, 0, 0, .29), -.5px .5px .5px rgba(0, 0, 0, .2); 4 | float: none; 5 | left: calc(199px + ((100vw - 199px) / 2) - 226px); 6 | padding: 3px 5px; 7 | position: absolute; 8 | margin: 0 auto; 9 | width: 100%; 10 | max-width: 432px; 11 | z-index: 999999999; 12 | top: 34px; 13 | 14 | &:not(.dialog-window) { 15 | z-index: 999; 16 | border-radius: 1.5px 1.5px 0 0; 17 | left: 194px; 18 | border: 1px solid $color-ddd; 19 | width: calc(100vw - 208px); 20 | box-shadow: none; 21 | max-width: none; 22 | margin-top: 2px; 23 | min-height: 25px; 24 | border-bottom: 0; 25 | padding: 0; 26 | 27 | form { 28 | padding: 1px 3px; 29 | } 30 | } 31 | 32 | i { 33 | margin-left: 4px; 34 | font-size: 15px; 35 | } 36 | 37 | button { 38 | min-width: 13%; 39 | width: auto; 40 | margin-left: 8px; 41 | line-height: 13px; 42 | margin-top: 2px; 43 | font-size: 12px; 44 | 45 | img { 46 | width: 12px; 47 | top: -1px; 48 | position: relative; 49 | margin: 0; 50 | } 51 | 52 | &.disabled { 53 | pointer-events: none; 54 | opacity: .5; 55 | } 56 | } 57 | 58 | &:not(.dialog-window) { 59 | button { 60 | width: 55px; 61 | min-width: auto; 62 | 63 | img { 64 | width: 13px; 65 | } 66 | 67 | &.button-close-prompt { 68 | width: 18px; 69 | max-width: 18px; 70 | min-width: 18px; 71 | text-indent: -2.5px; 72 | 73 | i { 74 | font-size: 11px; 75 | left: -6.5px; 76 | position: relative; 77 | float: left; 78 | } 79 | } 80 | } 81 | } 82 | 83 | select { 84 | width: 77px; 85 | height: 17px; 86 | font-size: 11px; 87 | position: relative; 88 | bottom: -1px; 89 | } 90 | 91 | input { 92 | 93 | &[type="checkbox"] { 94 | vertical-align: middle; 95 | margin: 0; 96 | } 97 | &[type='text'] { 98 | border: none; 99 | line-height: initial; 100 | border-radius: $border-radius-inputs; 101 | box-shadow: 0 1px 1px rgba(0, 0, 0, .25) inset; 102 | font-size: 12px; 103 | padding: 3px 4px 2.5px 4px; 104 | width: 18%; 105 | 106 | &:first-child { 107 | width: 57%; 108 | margin-right: 0; 109 | } 110 | } 111 | } 112 | 113 | .tags-input { 114 | line-height: initial; 115 | border-radius: 3px; 116 | box-shadow: 0 1px 1px rgba(0, 0, 0, .25) inset; 117 | font-size: 12px; 118 | padding: 3px 4px 2.5px 4px; 119 | width: 66%; 120 | background: white; 121 | white-space: nowrap; 122 | overflow: hidden; 123 | float: left; 124 | margin-right: 2%; 125 | 126 | //placeholder for fake contenteditable input 127 | &:empty:before { 128 | color: $color-muted; 129 | content: attr(placeholder); 130 | } 131 | br { 132 | display: none; 133 | } 134 | 135 | * { 136 | display: inline; 137 | white-space: nowrap; 138 | } 139 | 140 | b { 141 | font-weight: normal; 142 | position: relative; 143 | bottom: -.07em; 144 | } 145 | 146 | span { 147 | background: $bg-tags; 148 | border-radius: $border-radius-tags; 149 | border: $border-tags; 150 | padding: 0 3px 1px 3px; 151 | margin: 1px 0 0 0; 152 | } 153 | } 154 | 155 | &.dialog-window { 156 | height: calc(100vh - 39px); 157 | position: fixed; 158 | left: 190px; 159 | border: 0; 160 | box-shadow: none; 161 | top: 10px; 162 | width: 100%; 163 | padding: 0; 164 | border-radius: 0; 165 | background: white; 166 | z-index: 9999; 167 | max-width: calc(100vw - 200px); 168 | border: 1px solid $color-ccc; 169 | } 170 | 171 | .window { 172 | float: none; 173 | height: calc(100vh - 65px); 174 | overflow: auto; 175 | background: white; 176 | font-size: 12.5px; 177 | margin: 0 auto; 178 | margin-top: -4px; 179 | 180 | .logs { 181 | padding: 1px 6px; 182 | 183 | &:first-child { 184 | margin-top: 5px; 185 | } 186 | } 187 | .logs-error { 188 | color: $color-error; 189 | } 190 | 191 | .ng-ace-editor { 192 | height: 100%; 193 | } 194 | } 195 | } 196 | 197 | .prompt-history-item { 198 | line-height: 23px; 199 | width: 100%; 200 | margin: 0 auto; 201 | 202 | &:first-child { 203 | padding-top: 3px; 204 | } 205 | &:hover { 206 | &:not(.active) { 207 | background: $bg-lighter; 208 | } 209 | } 210 | 211 | &.active { 212 | background-color: $bg-muted-invisible; 213 | } 214 | 215 | i { 216 | font-size: 13px; 217 | color: $color-primary; 218 | 219 | &.fa-caret-right { 220 | color: $color-222; 221 | } 222 | } 223 | } 224 | 225 | .prompt-history-status { 226 | width: 100%; 227 | margin: 0 auto; 228 | float: none; 229 | height: 300px; 230 | overflow: auto; 231 | } 232 | 233 | .prompt-window-infos { 234 | font-size: 12.5px; 235 | max-width: 200px; 236 | white-space: nowrap; 237 | overflow: hidden; 238 | text-overflow: ellipsis; 239 | display: inline-block; 240 | } 241 | .prompt-window-holder { 242 | line-height: 100px; 243 | text-align: center; 244 | } 245 | .prompt-window-options { 246 | padding: 0 6px; 247 | width: 100%; 248 | display: inline-block; 249 | background: $bg-light; 250 | margin: 0 auto; 251 | line-height: 25px; 252 | float: none; 253 | height: 30px; 254 | 255 | i { 256 | margin-left: 0; 257 | color: $color-777; 258 | margin-right: 5px; 259 | font-size: 13px; 260 | } 261 | 262 | button { 263 | height: 19.5px; 264 | line-height: 13px; 265 | font-size: 12px; 266 | float: right; 267 | margin-top: 3px; 268 | } 269 | 270 | img { 271 | width: 13px; 272 | } 273 | 274 | .prompt-resize-holder { 275 | width: 130%; 276 | height: 40px; 277 | z-index: -1; 278 | position: absolute; 279 | bottom: -10px; 280 | left: -5%; 281 | 282 | &:hover, &:focus, &:active { 283 | cursor: ns-resize; 284 | } 285 | } 286 | } 287 | 288 | .prompt-search { 289 | font-size: 11.5px; 290 | margin-top: 3px; 291 | color: $color-muted; 292 | overflow: hidden; 293 | overflow-y: auto; 294 | max-height: 39.8vh; 295 | border-bottom: 1px solid $color-ddd; 296 | box-shadow: 10px -10px 10px $bg-eee inset; 297 | 298 | .prompt-search-loader { 299 | color: $color-222; 300 | padding: 1.5px 3.5px 2.5px 3.5px; 301 | 302 | img { 303 | vertical-align: bottom; 304 | margin: 0; 305 | margin-right: 2px; 306 | width: 16px; 307 | } 308 | } 309 | 310 | h5 { 311 | color: $color-222; 312 | margin: 0; 313 | padding: 0; 314 | margin-top: 5px; 315 | font-weight: 500; 316 | font-size: 12px; 317 | margin-bottom: 3px; 318 | } 319 | 320 | .prompt-search-item { 321 | padding: 3px 6px; 322 | border-bottom: 1px solid $bg-muted-invisible; 323 | 324 | &:last-child { 325 | border: 0; 326 | } 327 | 328 | &:hover { 329 | background: $bg-muted-invisible; 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | parserOptions: { 6 | ecmaVersion: 6, 7 | sourceType: "module" 8 | }, 9 | "rules": { 10 | "comma-dangle": [ 11 | 2, 12 | "never" 13 | ], 14 | "no-cond-assign": [ 15 | 2, 16 | "always" 17 | ], 18 | "no-console": 1, 19 | "no-constant-condition": 2, 20 | "no-control-regex": 2, 21 | "no-debugger": 2, 22 | "no-dupe-args": 2, 23 | "no-dupe-keys": 2, 24 | "no-duplicate-case": 2, 25 | "no-empty-character-class": 2, 26 | "no-empty": 2, 27 | "no-ex-assign": 1, 28 | "no-extra-boolean-cast": 1, 29 | "no-extra-parens": [ 30 | 2, 31 | "functions" 32 | ], 33 | "no-extra-semi": 2, 34 | "no-func-assign": 1, 35 | "no-inner-declarations": [ 36 | 2, 37 | "both" 38 | ], 39 | "no-invalid-regexp": 2, 40 | "no-irregular-whitespace": 2, 41 | "no-negated-in-lhs": 2, 42 | "no-obj-calls": 2, 43 | "no-regex-spaces": 2, 44 | "no-sparse-arrays": 2, 45 | "no-unexpected-multiline": 2, 46 | "no-unreachable": 2, 47 | "use-isnan": 2, 48 | "valid-typeof": 2, 49 | "accessor-pairs": 2, 50 | "block-scoped-var": 2, 51 | "consistent-return": 0, 52 | "curly": [ 53 | 2, 54 | "all" 55 | ], 56 | "default-case": 2, 57 | "dot-location": [ 58 | 2, 59 | "property" 60 | ], 61 | "dot-notation": 2, 62 | "eqeqeq": [ 63 | 2, 64 | "smart" 65 | ], 66 | "guard-for-in": 1, 67 | "no-alert": 1, 68 | "no-caller": 2, 69 | "no-case-declarations": 0, 70 | "no-div-regex": 2, 71 | "no-else-return": 2, 72 | "no-labels": 2, 73 | "no-empty-pattern": 2, 74 | "no-eq-null": 2, 75 | "no-eval": 2, 76 | "no-extend-native": 2, 77 | "no-extra-bind": 2, 78 | "no-fallthrough": 2, 79 | "no-floating-decimal": 2, 80 | "no-implicit-coercion": [2, 81 | { 82 | "boolean": true, 83 | "number": true, 84 | "string": true 85 | } 86 | ], 87 | "no-implied-eval": 2, 88 | "no-invalid-this": 2, 89 | "no-iterator": 2, 90 | "no-lone-blocks": 2, 91 | "no-loop-func": 2, 92 | "no-magic-numbers": 0, 93 | "no-multi-spaces": 2, 94 | "no-multi-str": 2, 95 | "no-native-reassign": 2, 96 | "no-new-func": 2, 97 | "no-new-wrappers": 2, 98 | "no-new": 2, 99 | "no-octal-escape": 2, 100 | "no-octal": 2, 101 | "no-param-reassign": 2, 102 | "no-process-env": 1, 103 | "no-proto": 2, 104 | "no-redeclare": [2, { 105 | "builtinGlobals": true 106 | } 107 | ], 108 | "no-return-assign": 2, 109 | "no-script-url": 2, 110 | "no-self-compare": 2, 111 | "no-sequences": 2, 112 | "no-throw-literal": 1, 113 | "no-unused-expressions": 2, 114 | "no-useless-call": 2, 115 | "no-useless-concat": 2, 116 | "no-void": 2, 117 | "no-with": 2, 118 | "radix": 2, 119 | "vars-on-top": 2, 120 | "wrap-iife": [ 121 | 2, 122 | "outside" 123 | ], 124 | "yoda": 2, 125 | "strict": [ 126 | 2, 127 | "safe" 128 | ], 129 | "no-catch-shadow": 2, 130 | "no-delete-var": 2, 131 | "no-label-var": 2, 132 | "no-shadow-restricted-names": 2, 133 | "no-shadow": [ 134 | 2, 135 | { 136 | "builtinGlobals": true 137 | } 138 | ], 139 | "no-undef-init": 2, 140 | "no-undef": 2, 141 | "no-unused-vars": [ 142 | 2, 143 | { 144 | "vars": "all", 145 | "args": "after-used" 146 | } 147 | ], 148 | "no-use-before-define": 2, 149 | "callback-return": 1, 150 | "handle-callback-err": 2, 151 | "no-new-require": 2, 152 | "no-path-concat": 2, 153 | "no-process-exit": 0, 154 | "no-sync": 1, 155 | "array-bracket-spacing": [ 156 | 2, 157 | "never" 158 | ], 159 | "block-spacing": [ 160 | 2, 161 | "always" 162 | ], 163 | "brace-style": [ 164 | 2, 165 | "1tbs", 166 | { 167 | "allowSingleLine": false 168 | } 169 | ], 170 | "camelcase": [ 171 | 2, 172 | { 173 | "properties": "always" 174 | } 175 | ], 176 | "comma-spacing": [ 177 | 2, 178 | { 179 | "before": false, 180 | "after": true 181 | } 182 | ], 183 | "comma-style": [ 184 | 2, 185 | "last", 186 | { 187 | "exceptions": { 188 | "VariableDeclaration": true 189 | } 190 | } 191 | ], 192 | "computed-property-spacing": [ 193 | 2, 194 | "never" 195 | ], 196 | "consistent-this": [ 197 | 2, 198 | "that" 199 | ], 200 | "eol-last": 2, 201 | "func-names": 1, 202 | "func-style": [ 203 | 1, 204 | "expression", 205 | { 206 | "allowArrowFunctions": true 207 | } 208 | ], 209 | "id-length": 1, 210 | "key-spacing": [ 211 | 2, 212 | { 213 | "beforeColon": false, 214 | "afterColon": true 215 | } 216 | ], 217 | "linebreak-style": [ 218 | 2, 219 | "unix" 220 | ], 221 | "new-cap": 2, 222 | "new-parens": 2, 223 | "newline-after-var": [ 224 | 2, 225 | "always" 226 | ], 227 | "no-array-constructor": 2, 228 | "no-bitwise": 2, 229 | "no-continue": 2, 230 | "no-lonely-if": 2, 231 | "no-mixed-spaces-and-tabs": 2, 232 | "no-multiple-empty-lines": [ 233 | 1, 234 | { 235 | "max": 2, 236 | "maxEOF": 1 237 | } 238 | ], 239 | "no-negated-condition": 1, 240 | "no-nested-ternary": 2, 241 | "no-new-object": 2, 242 | "no-plusplus": 2, 243 | "no-spaced-func": 2, 244 | "no-ternary": 0, 245 | "no-trailing-spaces": [ 246 | 2, 247 | { 248 | "skipBlankLines": false 249 | } 250 | ], 251 | "no-underscore-dangle": 2, 252 | "no-unneeded-ternary": 2, 253 | "object-curly-spacing": [ 254 | 2, 255 | "never" 256 | ], 257 | "one-var": 2, 258 | "operator-assignment": [ 259 | 2, 260 | "always" 261 | ], 262 | "operator-linebreak": [ 263 | 2, 264 | "after" 265 | ], 266 | "quote-props": 2, 267 | "quotes": [ 268 | 2, 269 | "single", 270 | "avoid-escape" 271 | ], 272 | "semi-spacing": 2, 273 | "semi": [ 274 | 2, 275 | "always" 276 | ], 277 | "keyword-spacing": [ 278 | 2, 279 | { 280 | "before": true, 281 | "after": true 282 | } 283 | ], 284 | "space-before-blocks": [ 285 | 2, 286 | "always" 287 | ], 288 | "space-before-function-paren": [ 289 | 2, 290 | "never" 291 | ], 292 | "space-in-parens": [ 293 | 2, 294 | "never" 295 | ], 296 | "space-infix-ops": 2, 297 | "space-unary-ops": [ 298 | 2, 299 | { 300 | "words": true, 301 | "nonwords": false 302 | } 303 | ], 304 | "wrap-regex": 2, 305 | "arrow-parens": [ 306 | 2, 307 | "as-needed" 308 | ], 309 | "arrow-spacing": [ 310 | 2, 311 | { 312 | "before": true, 313 | "after": true 314 | } 315 | ], 316 | "constructor-super": 2, 317 | "generator-star-spacing": [ 318 | 2, 319 | { 320 | "before": false, 321 | "after": true 322 | } 323 | ], 324 | "no-confusing-arrow": 1, 325 | "no-class-assign": 2, 326 | "no-const-assign": 2, 327 | "no-dupe-class-members": 1, 328 | "no-this-before-super": 2, 329 | "no-var": 1, 330 | "object-shorthand": [ 331 | 2, 332 | "always" 333 | ], 334 | "prefer-arrow-callback": 1, 335 | "prefer-const": 1, 336 | "prefer-spread": 1, 337 | "prefer-template": 2, 338 | "require-yield": 2 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /lib/js/npm/npm-api.js: -------------------------------------------------------------------------------- 1 | /*global require navigator process,__dirname*/ 2 | import angular from 'angular'; 3 | const moduleName = 'npm-api.service' 4 | , fs = require('fs') 5 | , path = require('path') 6 | , cp = require('child_process'); 7 | 8 | angular.module(moduleName, []) 9 | .service('npm', /*@ngInject*/ function NpmService($rootScope, $log, errorsService) { 10 | const configureNpm = (folder, isGlobal) => new Promise(resolve => { 11 | const forkFactory = (command, param1, param2) => new Promise((forkFactoryResolved, forkFactoryRejected) => { 12 | const theParams = [folder, isGlobal] 13 | .concat([command, param1, param2]) 14 | .map(element => { 15 | 16 | if (Object.prototype.toString.call(element) === '[object Object]') { 17 | 18 | return JSON.stringify(element); 19 | } 20 | 21 | return element; 22 | }) 23 | , child = cp.fork(path.resolve(__dirname, 'npm-runner.js'), theParams, { 24 | 'cwd': __dirname, 25 | 'silent': true 26 | }); 27 | 28 | child.stdout.on('data', data => { 29 | $log.info(`stdout: ${data}`); 30 | }); 31 | 32 | child.stderr.on('data', data => { 33 | forkFactoryRejected(data); 34 | $log.error(`stderr: ${data}`); 35 | }); 36 | 37 | child.on('close', code => { 38 | $rootScope.$emit('npm:log:end', { 39 | 'type': command, 40 | 'message': code 41 | }); 42 | $log.info(`End of command ${command}`); 43 | $log.info(`child process exited with code ${code}`); 44 | }); 45 | 46 | child.on('message', message => { 47 | 48 | if (command === message.type) { 49 | 50 | forkFactoryResolved(message.payload); 51 | } else if (message.type === 'log') { 52 | 53 | $rootScope.$emit('npm:log:log', { 54 | 'type': command, 55 | 'data': message.payload 56 | }); 57 | } else { 58 | 59 | $log.debug(message); 60 | } 61 | }); 62 | }); 63 | 64 | resolve({ 65 | 'ping': () => { 66 | return forkFactory('ping'); 67 | }, 68 | 'launchInstall': () => { 69 | return forkFactory('launchInstall'); 70 | }, 71 | 'search': keyword => { 72 | return forkFactory('search', keyword); 73 | }, 74 | 'run': scriptName => { 75 | return forkFactory('run', scriptName); 76 | }, 77 | 'view': packageName => { 78 | return forkFactory('view', packageName); 79 | }, 80 | 'build': buildFolder => { 81 | return forkFactory('build', buildFolder); 82 | }, 83 | 'rebuild': () => { 84 | return forkFactory('rebuild'); 85 | }, 86 | 'install': (dependency, version) => { 87 | return forkFactory('install', dependency, version); 88 | }, 89 | 'installLatest': dependency => { 90 | return forkFactory('installLatest', dependency); 91 | }, 92 | 'update': dependency => { 93 | return forkFactory('update', dependency); 94 | }, 95 | 'rm': dependency => { 96 | return forkFactory('rm', dependency); 97 | }, 98 | 'listOutdated': () => { 99 | return forkFactory('listOutdated'); 100 | }, 101 | 'outdated': () => { 102 | return forkFactory('outdated'); 103 | }, 104 | 'prune': () => { 105 | return forkFactory('prune'); 106 | }, 107 | 'dedupe': () => { 108 | return forkFactory('dedupe'); 109 | }, 110 | 'list': () => { 111 | return forkFactory('list'); 112 | }, 113 | 'shrinkwrap': () => { 114 | return forkFactory('shrinkwrap'); 115 | }, 116 | 'doctor': () => { 117 | return forkFactory('doctor'); 118 | }, 119 | 'root': () => { 120 | return forkFactory('root'); 121 | } 122 | }); 123 | }) 124 | , getNpmVersion = () => new Promise(resolve => { 125 | cp.exec('npm -v', (err, stdout, stderr) => { 126 | let npmVersion; 127 | 128 | $log.warn(err, stderr); 129 | 130 | if (stdout && 131 | stdout.length > 0) { 132 | npmVersion = stdout.toString(); 133 | } 134 | resolve(npmVersion); 135 | }); 136 | }) 137 | , isNpmGloballyInstalled = () => new Promise((resolve, reject) => { 138 | cp.exec('npm -v', err => { 139 | if (err) { 140 | reject(err); 141 | } else { 142 | resolve(); 143 | } 144 | }); 145 | }) 146 | , pingRegistry = () => new Promise((resolve, reject) => { 147 | if (navigator.onLine) { 148 | return configureNpm('').then(npm => { 149 | 150 | return npm.ping() 151 | .then(resolve) 152 | .catch(reject); 153 | }) 154 | .catch(err => { 155 | $log.warn('Ping registry', err); 156 | reject(err); 157 | }); 158 | } 159 | 160 | $log.warn('You are offline: unable to ping registry.'); 161 | return reject(); 162 | }) 163 | , outdatedGlobalVersion = () => new Promise(resolve => { 164 | this.npmGlobal() 165 | .catch(error => errorsService.showErrorBox('Npm error', `Error during configuring npm for asking the globally installed version: ${error}`)) 166 | .then(npmInFolder => { 167 | 168 | return npmInFolder.outdated() 169 | .then(resolve) 170 | .catch(error => errorsService.showErrorBox('Npm error', `Error asking the globally installed version: ${error}`)); 171 | }); 172 | }); 173 | 174 | this.updateNpmGlobally = () => { 175 | const npmLib = { 176 | 'name': 'npm' 177 | }; 178 | 179 | return new Promise((resolve, reject) => { 180 | 181 | this.npmGlobal() 182 | .catch(error => errorsService.showErrorBox('Npm error', `Error configuring npm for installing latest ${npmLib.name}: ${error} `)) 183 | .then(npmInFolder => { 184 | 185 | $rootScope.$emit('npm:log:start'); 186 | npmInFolder.installLatest(npmLib) 187 | .then(() => { 188 | 189 | $rootScope.$emit('npm:log:stop'); 190 | resolve(); 191 | }) 192 | .catch(error => { 193 | 194 | $rootScope.$emit('npm:log:stop'); 195 | reject(error); 196 | }); 197 | }); 198 | }); 199 | }; 200 | 201 | $rootScope.$on('dom:ready', () => { 202 | $log.info('DOM is ready for npm'); 203 | //sync shell path or app will not work, yep. 204 | process.env.PATH = require('shell-path').sync(); 205 | 206 | cp.exec('npm root -g', (err, stdout, stderr) => { 207 | 208 | if (err) { 209 | 210 | throw new Error(err); 211 | } 212 | 213 | if (stderr) { 214 | 215 | $log.warn(stderr); 216 | } 217 | 218 | let globalFolder = stdout 219 | , nodeModulesExt = ''; //important 220 | 221 | if (process.platform != 'win32') { 222 | 223 | globalFolder = globalFolder.replace('/lib/node_modules', ''); 224 | } else { 225 | 226 | globalFolder = globalFolder.replace('node_modules', ''); 227 | } 228 | 229 | globalFolder = globalFolder.trim(); 230 | 231 | this.npmGlobal = () => { 232 | 233 | return configureNpm(globalFolder, true); 234 | }; 235 | 236 | if (process.platform && 237 | process.platform !== 'win32') { 238 | //on windows it doesn't exists 239 | nodeModulesExt = '/lib/node_modules'; 240 | } 241 | 242 | fs.stat(`${globalFolder}${nodeModulesExt}`, (statError, stats) => { 243 | 244 | if (statError) { 245 | 246 | $log.error(statError); 247 | } 248 | 249 | if (process.platform === 'win32') { 250 | //win if error occur it means that folder is not writable and so we set the check to fail. 251 | if (statError) { 252 | $rootScope.$apply(scope => { 253 | 254 | scope.$emit('npm:global-privilege-check', { 255 | 'user': -1, 256 | 'processUser': 1, 257 | 'group': -1, 258 | 'processGroup': 1 259 | }); 260 | }); 261 | } 262 | 263 | return $rootScope.$apply(scope => { 264 | 265 | scope.$emit('npm:global-privilege-check', { 266 | 'user': 1, 267 | 'processUser': 1, 268 | 'group': 1, 269 | 'processGroup': 1 270 | }); 271 | }); 272 | } 273 | 274 | return $rootScope.$apply(scope => { 275 | 276 | scope.$emit('npm:global-privilege-check', { 277 | 'user': stats.uid, 278 | 'processUser': process.getuid(), 279 | 'group': stats.gid, 280 | 'processGroup': process.getgid() 281 | }); 282 | }); 283 | }); 284 | 285 | $rootScope.$emit('npm:ready'); 286 | }); 287 | }); 288 | 289 | this.npmInFolder = configureNpm; 290 | this.outdatedGlobalVersion = outdatedGlobalVersion; 291 | this.getNpmVersion = getNpmVersion; 292 | this.isNpmGloballyInstalled = isNpmGloballyInstalled; 293 | this.pingRegistry = pingRegistry; 294 | }); 295 | 296 | export default moduleName; 297 | -------------------------------------------------------------------------------- /lib/icons/fontello/font/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2017 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ndm 2 | 3 | ![screenshot-npm-desktop-manager](http://i.imgur.com/6KL3pt7.png) 4 | 5 | 6 | The Open Source npm desktop GUI. 7 | 8 | Runs on Linux, MacOS and Windows. 9 | 10 | **ndm** stands for **"npm desktop manager"**. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |

30 | About ndm   31 | | 32 |   Develop it  | 33 |   Build it  34 | | 35 |   Contribute  36 | | 37 |   Recommendations  38 | | 39 |   FAQ  40 | | 41 |   License 42 |

43 | 44 | 45 | ## Download 46 | **[Download for MacOS](https://720kb.github.io/ndm#mac)**   |  **[Download for Linux](https://720kb.github.io/ndm#linux)**   |  **[Download for Windows](https://720kb.github.io/ndm#win)**   47 | 48 | ###### You can browse all the releases at [github.com/720kb/ndm/releases](https://github.com/720kb/ndm/releases) 49 | 50 | 51 | 52 | ## Homebrew 53 | 54 | On MacOS you can install **ndm** also with [Homebrew Cask](https://caskroom.github.io/): 55 | 56 | ```bash 57 | $ brew update 58 | $ brew cask install ndm 59 | ``` 60 | 61 | ## Arch Linux 62 | 63 | On Linux you can install **ndm** also like this: 64 | 65 | ```bash 66 | $ yaourt -S ndm 67 | ``` 68 | 69 | ## Debian 70 | 71 | On Debian based linux is possible to install **ndm** doing: 72 | 73 | ```bash 74 | $ echo "deb [trusted=yes] https://apt.fury.io/720kb/ /" | sudo tee 75 | /etc/apt/sources.list.d/ndm.list && sudo apt-get update && sudo apt-get install ndm 76 | ``` 77 | 78 | ## RedHat 79 | 80 | On RedHat based linux is possible to install **ndm** doing: 81 | 82 | ```bash 83 | echo "[fury] 84 | name=ndm repository 85 | baseurl=https://repo.fury.io/720kb/ 86 | enabled=1 87 | gpgcheck=0" | sudo tee /etc/yum.repos.d/ndm.repo && sudo yum update && sudo yum install ndm 88 | ``` 89 | 90 | **Core team** 91 | [720kb](https://720kb.net) 92 | 93 | **Contributors** [All the awesome contributors](https://github.com/720kb/ndm/graphs/contributors) 94 | 95 | 96 | ## Support ndm 97 | 98 | > Donating to an open source project is the best way to tie your love for it. 99 | 100 | If you enjoy **ndm** consider donating to the project and help mantain and continuously improve it! 101 | 102 | **Backers** 103 | 104 | Support us with a monthly donation and help us continue our activities. [Become a backer](https://opencollective.com/ndm#backer) 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | **Sponsors** 138 | 139 | Become a sponsor and get your logo on our README on Github with a link to your site. [Become a sponsor](https://opencollective.com/ndm#sponsor) 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /lib/js/npm/npm-operations.js: -------------------------------------------------------------------------------- 1 | /* global process,require */ 2 | const isGlobalSym = Symbol('isGlobal') 3 | , npmSym = Symbol('npm') 4 | , whereSym = Symbol('where') 5 | , fetch = require('node-fetch') 6 | , swapFolderAndGlobal = function SwapFolderAndGlobal(prefix, isGlobal) { 7 | 8 | const oldPrefix = this[npmSym].config.prefix 9 | , oldGlobal = this[npmSym].config.global; 10 | 11 | this[npmSym].config.prefix = prefix; 12 | this[npmSym].config.global = isGlobal; 13 | 14 | return [oldPrefix, oldGlobal]; 15 | }; 16 | 17 | 18 | class NpmOperations { 19 | 20 | constructor(where, configuredNpm, isGlobal) { 21 | 22 | this[whereSym] = where; 23 | this[npmSym] = configuredNpm; 24 | this[isGlobalSym] = isGlobal; 25 | } 26 | 27 | ping() { 28 | return new Promise((resolvePing, rejectPing) => { 29 | this[npmSym].commands.ping('', err => { 30 | if (err) { 31 | 32 | return rejectPing(err); 33 | } 34 | 35 | return resolvePing(); 36 | }); 37 | }); 38 | } 39 | 40 | launchInstall() { 41 | 42 | return new Promise((resolveInstall, rejectInstall) => { 43 | this[npmSym].commands.install(this[whereSym], [], err => { 44 | if (err) { 45 | 46 | return rejectInstall(err); 47 | } 48 | 49 | return resolveInstall(); 50 | }); 51 | }); 52 | } 53 | 54 | search(keyword) { 55 | 56 | return fetch(`https://registry.npmjs.org/-/v1/search?text=${keyword}&size=25`) 57 | .then(res => res.json()); 58 | } 59 | 60 | run(scriptName) { 61 | return new Promise((resolveRun, rejectRun) => { 62 | this[npmSym].commands.runScript([scriptName], (err, infos) => { 63 | if (err) { 64 | 65 | return rejectRun(err); 66 | } 67 | 68 | return resolveRun(infos); 69 | }); 70 | }); 71 | } 72 | 73 | view(packageName) { 74 | return new Promise((resolveView, rejectView) => { 75 | 76 | this[npmSym].commands.view([packageName], (err, infos) => { 77 | if (err) { 78 | 79 | return rejectView(err); 80 | } 81 | 82 | return resolveView(infos); 83 | }); 84 | }); 85 | } 86 | 87 | build(folder) { 88 | 89 | return new Promise((resolveBuild, rejectBuild) => { 90 | 91 | this[npmSym].commands.build([folder], (err, infos) => { 92 | if (err) { 93 | 94 | return rejectBuild(err); 95 | } 96 | 97 | return resolveBuild(infos); 98 | }); 99 | }); 100 | } 101 | 102 | rebuild() { 103 | 104 | return new Promise((resolveRebuild, rejectRebuild) => { 105 | 106 | this[npmSym].commands.rebuild([], (err, infos) => { 107 | if (err) { 108 | return rejectRebuild(err); 109 | } 110 | return resolveRebuild(infos); 111 | }); 112 | }); 113 | } 114 | 115 | install(dependency, version) { 116 | let dependencyToSubmit = dependency.name; 117 | 118 | if (version && 119 | version !== 'false' && 120 | version !== 'undefined') { 121 | 122 | dependencyToSubmit += `@${version}`; 123 | } 124 | 125 | return new Promise((resolveInstall, rejectInstall) => { 126 | const toInstall = [dependencyToSubmit] 127 | , whereToInstall = this[isGlobalSym] ? '' : this[whereSym]; 128 | 129 | if (!this[isGlobalSym] && 130 | dependency.kind === 'dev') { 131 | 132 | this[npmSym].config.set('save-dev', true); 133 | } else if (!this[isGlobalSym]) { 134 | 135 | this[npmSym].config.set('save', true); 136 | } 137 | 138 | this[npmSym].commands.install(whereToInstall, toInstall, err => { 139 | 140 | if (err) { 141 | 142 | return rejectInstall(err); 143 | } 144 | 145 | if (!this[isGlobalSym]) { 146 | 147 | this[npmSym].config.set('save-dev', false); 148 | this[npmSym].config.set('save', false); 149 | } 150 | return resolveInstall(); 151 | }); 152 | }); 153 | } 154 | 155 | installLatest(dependency) { 156 | 157 | return this.install(dependency, dependency.latest); 158 | } 159 | 160 | update(dependency) { 161 | 162 | return new Promise((resolveUpdate, rejectUpdate) => { 163 | const toUpdate = [dependency.name]; 164 | 165 | if (!this[isGlobalSym] && 166 | dependency.kind === 'dev') { 167 | 168 | this[npmSym].config.set('save-dev', true); 169 | } else if (!this[isGlobalSym]) { 170 | 171 | this[npmSym].config.set('save', true); 172 | } 173 | 174 | this[npmSym].commands.update(toUpdate, err => { 175 | 176 | if (err) { 177 | 178 | return rejectUpdate(err); 179 | } 180 | 181 | if (!this[isGlobalSym]) { 182 | 183 | this[npmSym].config.set('save-dev', false); 184 | this[npmSym].config.set('save', false); 185 | } 186 | return resolveUpdate(); 187 | }); 188 | }); 189 | } 190 | 191 | rm(dependency) { 192 | 193 | return new Promise((resolveRm, rejectRm) => { 194 | const toRemove = [dependency.name]; 195 | 196 | if (!this[isGlobalSym] && 197 | dependency.kind === 'dev') { 198 | 199 | this[npmSym].config.set('save-dev', true); 200 | } else if (!this[isGlobalSym]) { 201 | 202 | this[npmSym].config.set('save', true); 203 | } 204 | 205 | this[npmSym].commands.rm(toRemove, err => { 206 | 207 | if (err) { 208 | 209 | return rejectRm(err); 210 | } 211 | 212 | if (!this[isGlobalSym]) { 213 | 214 | this[npmSym].config.set('save-dev', false); 215 | this[npmSym].config.set('save', false); 216 | } 217 | return resolveRm(); 218 | }); 219 | }); 220 | } 221 | 222 | listOutdated() { 223 | return new Promise((listOutdatedResolve, listOutdatedReject) => { 224 | 225 | Promise.all([this.list(), this.outdated()]) 226 | .then(resolved => { 227 | 228 | if (resolved && 229 | Array.isArray(resolved) && 230 | resolved.length === 2) { 231 | const outdatedList = resolved[1] 232 | , listList = resolved[0]; 233 | let toResolve = []; 234 | 235 | listList.forEach(element => { 236 | 237 | if (element && 238 | element.name) { 239 | const outdatedData = outdatedList.filter(filterElement => { 240 | return filterElement && filterElement.name === element.name; 241 | }).map(mapElement => ({ 242 | 'name': element.name, 243 | 'kind': element.kind, 244 | 'current': mapElement.current, 245 | 'wanted': mapElement.wanted, 246 | 'latest': mapElement.latest 247 | })); 248 | 249 | if (outdatedData.length > 0) { 250 | 251 | toResolve = toResolve.concat(outdatedData); 252 | } else { 253 | 254 | toResolve.push(element); 255 | } 256 | } 257 | }); 258 | 259 | return listOutdatedResolve(toResolve); 260 | } 261 | 262 | return listOutdatedReject('Output from list and oudated commands wrong!'); 263 | }) 264 | .catch(err => listOutdatedReject(err)); 265 | }); 266 | } 267 | 268 | outdated() { 269 | return new Promise((resolveOutdated, rejectOutdated) => { 270 | const [oldPrefix, oldGlobal] = swapFolderAndGlobal.apply(this, [this[whereSym], this[isGlobalSym]]); 271 | 272 | this[npmSym].commands.outdated([], true, (outdatedError, packageInformations) => { 273 | 274 | if (outdatedError) { 275 | 276 | this[npmSym].config.prefix = oldPrefix; 277 | this[npmSym].config.global = oldGlobal; 278 | return rejectOutdated(outdatedError); 279 | } 280 | 281 | if (packageInformations && 282 | Array.isArray(packageInformations)) { 283 | const toResolve = []; 284 | 285 | for (const aPackageInformation of packageInformations) { 286 | 287 | toResolve.push({ 288 | 'name': aPackageInformation[1], 289 | 'current': aPackageInformation[2], 290 | 'wanted': aPackageInformation[3], 291 | 'latest': aPackageInformation[4] 292 | }); 293 | } 294 | 295 | this[npmSym].config.prefix = oldPrefix; 296 | this[npmSym].config.global = oldGlobal; 297 | return resolveOutdated(toResolve); 298 | } 299 | 300 | return rejectOutdated('Package informations from outdated are wrong!'); 301 | }); 302 | }); 303 | } 304 | 305 | prune() { 306 | return new Promise((resolvePrune, rejectPrune) => { 307 | const oldCWD = process.cwd(); 308 | 309 | process.chdir(this[whereSym]); 310 | this[npmSym].commands.prune([], (pruneError, packageInformations) => { 311 | 312 | process.chdir(oldCWD); 313 | if (pruneError) { 314 | 315 | return rejectPrune(pruneError); 316 | } 317 | 318 | return resolvePrune(packageInformations); 319 | }); 320 | }); 321 | } 322 | 323 | dedupe() { 324 | return new Promise((resolveDedupe, rejectDedupe) => { 325 | 326 | this[npmSym].commands.dedupe([], (DedupeError, packageInformations) => { 327 | 328 | if (DedupeError) { 329 | 330 | return rejectDedupe(DedupeError); 331 | } 332 | 333 | return resolveDedupe(packageInformations); 334 | }); 335 | }); 336 | } 337 | 338 | list() { 339 | return new Promise((resolveList, rejectList) => { 340 | const [oldPrefix, oldGlobal] = swapFolderAndGlobal.apply(this, [this[whereSym], this[isGlobalSym]]); 341 | 342 | this[npmSym].commands.list([], true, (listError, packageInformations) => { 343 | 344 | if (listError) { 345 | 346 | this[npmSym].config.prefix = oldPrefix; 347 | this[npmSym].config.global = oldGlobal; 348 | return rejectList(listError); 349 | } 350 | 351 | if (packageInformations && 352 | packageInformations.dependencies && 353 | packageInformations.devDependencies) { 354 | const toResolve = [] 355 | , dependenciesKeys = Object.keys(packageInformations.dependencies) 356 | , dependenciesKeysLength = dependenciesKeys.length 357 | , devDependenciesKeys = Object.keys(packageInformations.devDependencies) 358 | , filteringFunction = function FilteringFunction(element) { 359 | 360 | return element && element.name !== packageInformations.dependencies[this]; 361 | } 362 | , filterIsADevDependency = function filterIsADevDependency(aDependency, element) { 363 | 364 | return element && element === aDependency; 365 | }; 366 | 367 | for (let dependenciesKeysIndex = 0; dependenciesKeysIndex < dependenciesKeysLength; dependenciesKeysIndex += 1) { 368 | const aDependencyKey = dependenciesKeys[dependenciesKeysIndex]; 369 | 370 | if (aDependencyKey && 371 | toResolve.every(filteringFunction, aDependencyKey)) { 372 | const aDependency = packageInformations.dependencies[aDependencyKey] 373 | , isADevDependency = devDependenciesKeys.filter(filterIsADevDependency.bind(devDependenciesKeys, aDependencyKey)); 374 | 375 | toResolve.push({ 376 | 'name': aDependencyKey, 377 | 'current': aDependency.version, 378 | 'kind': isADevDependency[0] ? 'dev' : '' 379 | }); 380 | } 381 | } 382 | 383 | this[npmSym].config.prefix = oldPrefix; 384 | this[npmSym].config.global = oldGlobal; 385 | return resolveList(toResolve); 386 | } 387 | 388 | return rejectList('Package informations from list command are wrong!'); 389 | }); 390 | }); 391 | } 392 | 393 | shrinkwrap() { 394 | return new Promise((resolveShrink, rejectShrink) => { 395 | 396 | this[npmSym].commands.shrinkwrap([], (shrinkError, infos) => { 397 | 398 | if (shrinkError) { 399 | 400 | return rejectShrink(shrinkError); 401 | } 402 | 403 | return resolveShrink(infos); 404 | }); 405 | }); 406 | } 407 | 408 | doctor() { 409 | return new Promise((resolveDoctor, rejectDoctor) => { 410 | 411 | this[npmSym].commands.doctor((doctorErr, doctorInfo) => { 412 | 413 | if (doctorErr) { 414 | 415 | return rejectDoctor(doctorErr); 416 | } 417 | 418 | return resolveDoctor(doctorInfo); 419 | }); 420 | }); 421 | } 422 | 423 | root() { 424 | return new Promise((resolveRoot, rejectRoot) => { 425 | 426 | this[npmSym].commands.root([], (rootError, rootInfo) => { 427 | 428 | if (rootError) { 429 | 430 | return rejectRoot(rootError); 431 | } 432 | 433 | return resolveRoot(rootInfo); 434 | }); 435 | }); 436 | } 437 | } 438 | 439 | export default NpmOperations; 440 | --------------------------------------------------------------------------------