├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _config.yml ├── bin └── index.js ├── images ├── logo-192.png ├── logo-512.png └── terminal.gif ├── lib ├── actions.js ├── colors.js ├── commands │ ├── add.js │ ├── create.js │ ├── edit.js │ ├── getpath.js │ ├── open.js │ ├── remove.js │ ├── rmeditor.js │ └── seteditor.js ├── helper.js └── logs.js ├── mocharc.json ├── npm-shrinkwrap.json ├── package.json ├── tests ├── action.spec.js ├── helper.spec.js ├── open.spec.js └── utils │ └── cleanup.js └── types └── utils.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | old-code/ 3 | dev/ 4 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | jest: true 6 | }, 7 | extends: ['google', 'prettier'], 8 | plugins: ['prettier'], 9 | globals: { 10 | Atomics: 'readonly', 11 | SharedArrayBuffer: 'readonly' 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018 15 | }, 16 | rules: { 17 | 'prettier/prettier': ['error'], 18 | 'comma-dangle': 0, 19 | 'require-jsdoc': 0, 20 | 'no-trailing-spaces': 0, 21 | 'arrow-parens': 0, 22 | 'guard-for-in': 0, 23 | 'valid-jsdoc': 0, 24 | 'no-undef': 'error', 25 | 'operator-linebreak': [ 26 | 'error', 27 | 'after', 28 | { 29 | overrides: { '?': 'ignore', ':': 'ignore', '+': 'ignore' } 30 | } 31 | ], 32 | indent: [ 33 | 'error', 34 | 2, 35 | { 36 | CallExpression: { arguments: 'first' }, 37 | ignoredNodes: [ 38 | 'CallExpression > CallExpression', 39 | 'CallExpression > MemberExpression' 40 | ], 41 | SwitchCase: 1 42 | } 43 | ], 44 | 'max-len': ['error', { code: 80, ignoreComments: true }] 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | dist 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # OSX specific files 65 | .DS_Store 66 | 67 | test-app -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | mocharc.json 3 | build 4 | dist 5 | _config.yml 6 | images 7 | .eslintrc.js 8 | .eslintignore 9 | .prettierignore 10 | .prettierrc 11 | .vscode/ 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | old-code/ 3 | dev/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "none" 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Production Releases 2 | (Note: Only production releases will be mentioned here, If you want to see beta releases, you can find them [here](https://github.com/saurabhdaware/projectman/releases)) 3 | 4 | ### v2.0.0 *[LATEST RELEASE]* 5 | 6 | - Add `pm create` command 7 | - Remove support for binaries 8 | 9 | 10 | ### v1.3.3 11 | 12 | Command suggestions added (Thanks [@jamesgeorge007](https://github.com/jamesgeorge007) for [#PR32](https://github.com/saurabhdaware/projectman/pull/32)) 13 | 14 | 15 | ### v1.3.2 16 | - `pm getpath` Question fix 17 | - prompts updated to v2.3.0 18 | - Unit testing for helper added ([@junaidrahim](https://github.com/junaidrahim) - [#27](https://github.com/saurabhdaware/projectman/pull/27)) 19 | 20 | 21 | ### v1.3.1 22 | 23 | Documentation Update 24 | 25 | 26 | 27 | ### v1.3.0 28 | --- 29 | ```shell 30 | npm install -g projectman 31 | ``` 32 | **Release Date:** 20th Sept, 2019 33 | 34 | --- 35 | 36 | - ## Brought dependencies from 36 packages to 4 packages!! 🎉 37 | Some internal code refactoring and asking myself "Do I really need this package?" helped me bring down dependency tree of 37 packages to 4 packages!!! 38 | - ## AutoComplete added during project selection. 39 | In `pm open` and other project selection menus. You can now start typing the letters and list will be filtered out to show projects matching the letters. 40 | - ## Ability to open/add URLs (Thank You [@ZakariaTalhami](https://github.com/ZakariaTalhami) for PR [#20](https://github.com/saurabhdaware/projectman/pull/20) ) 41 | - `pm add --url [URL]` to add URLs to the projectman. `[URL]` is an optional parameter 42 | - These URLs will show up in `pm open` with (URL) appended to their names. 43 | - On selecting the URL in `pm open` they will be opened in your default browser. 44 | - This can be used to store your repositories/websites/other useful links. 45 | 46 | --- 47 | --- 48 | ### v1.2.0 49 | ```shell 50 | npm install -g projectman@1.2.0 51 | ``` 52 | **Release Date:** 12th Sept, 2019 53 | 54 | --- 55 | - [Presenting ProjectMan Binaries](#presenting-projectman-binaries) 56 | - [`cd` to directory without opening the project](#cd-to-directory-without-opening-the-project) 57 | - [Added `--for-project` flag in `pm seteditor`](#added-for-project-flag-in-pm-seteditor) 58 | - [`Other` option addbed in `pm seteditor`](#other-option-added-in-pm-seteditor) 59 | - [New command `pm rmeditor`](#new-command-pm-rmeditor) 60 | --- 61 | 62 | #### > Presenting ProjectMan binaries 🎉🦸 63 | Why should Node developers have all the fun? Now use ProjectMan without NodeJS or NPM installed. 64 | 65 | Download binaries and follow installation instructions given with them : 66 | 67 | [![Download button for windows](https://img.shields.io/badge/for_windows-0099ff?style=for-the-badge&logo=windows)](https://apps.saurabhdaware.in/projectman#windows) [![Download button for Linux](https://img.shields.io/badge/for_linux-032f62?style=for-the-badge&logo=linux&logoColor=white)](https://apps.saurabhdaware.in/projectman/#linux-and-mac) [![Download button for MACOS](https://img.shields.io/badge/for_macos-111111?style=for-the-badge&logo=apple&logoColor=white)](https://apps.saurabhdaware.in/projectman/#linux-and-mac) 68 | 69 | #### > `cd` to directory without opening the project. 70 | ```shell 71 | cd $(pm getpath) 72 | ``` 73 | This will allow users to jump to a directory in command line without opening the project. 74 | 75 | **PR :** [#9](https://github.com/saurabhdaware/projectman/pull/9) (Thank You [@ZakariaTalhami](https://github.com/ZakariaTalhami)) 76 | **Issue :** [#5](https://github.com/saurabhdaware/projectman/issues/5) (Thank you [@feitzi](https://github.com/feitzi)) 77 | 78 | #### > Added `--for-project` flag in `pm seteditor`: 79 | ```shell 80 | pm seteditor --for-project 81 | ``` 82 | This will allow users to set different editor for a specific project 83 | E.g. Set Atom for `Project1` and have VSCode for other projects 84 | **Isssue :** [#13](https://github.com/saurabhdaware/projectman/issues/13) 85 | **PR :** [#16](https://github.com/saurabhdaware/projectman/pull/16) 86 | 87 | #### > `Other` option added in `pm seteditor` : 88 | You can now select `other` option and type the editorCommand as an input rather than typing `pm seteditor [editorCommand]` 89 | 90 | #### > New command `pm rmeditor`: 91 | ```shell 92 | pm rmeditor 93 | ``` 94 | This will allow users to remove the project specific editors. 95 | You can either `pm rmeditor` and choose the project to remove editor or `pm rmeditor --all` to remove all project specific editors. 96 | 97 | --- 98 | 99 | ## v1.1.0 100 | ```shell 101 | npm install -g projectman@1.1.0 102 | ``` 103 | - ***Project specific editors*** (Thanks [#4](https://github.com/saurabhdaware/projectman/issues/4) [@fechy](https://github.com/fechy) for issue) 104 | - Now you can `pm edit` and set `editor` key in `settings.json` projects[] with the value of the command of your editor. 105 | - `settings.json` E.g. 106 | ```js 107 | { 108 | "commandToOpen": "code", 109 | "projects": [ 110 | { 111 | "name":"MyCoolProject", 112 | "path":"/home/path/projects/mycoolproject", 113 | "editor":"vim" 114 | }, 115 | { 116 | "name":"TwoProject", 117 | "path":"/path/something/project" 118 | } 119 | ] 120 | } 121 | ``` 122 | This will allow users to open other projects in **VSCode** but use **Vim** to open `MyCoolProject` 123 | 124 | - ***Projects will not be erased after updating furthur*** 125 | (However they will still be erased while installing this update so I'll recommend to take copy of your settings.json If you have added multiple projects already) (Sorry but this is the last time when you'll have to do this :cry: )) 126 | (Thank you [@codyaverett](https://github.com/codyaverett) and [@Tanuj69](https://github.com/Tanuj69) issue [#2](https://github.com/saurabhdaware/projectman/issues/2) and helping me out solving this) 127 | - ***Added `vim` in `pm seteditor`*** 128 | also added a message explaining 'How to set editors/IDE that are not listed in the menu' 129 | - `pm` is now alias for `pm open` 130 | (Thank you [@johannesjo](https://github.com/johannesjo) for suggestion) 131 | - **`pm seteditor [commandToOpenEditor]` added** 132 | This will set default editor command, This can be used when the editor you want to use is not listed in `pm seteditor`. (Note: `pm seteditor` will work exactly same as it did before) 133 | 134 | --- 135 | 136 | ## v1.0.0 137 | ```shell 138 | npm install -g projectman@1.0.0 139 | ``` 140 | 141 | Initial release of ProjectMan. 142 | 143 | Includes following commands: 144 | 145 | ```shell 146 | pm add 147 | pm remove 148 | pm open 149 | pm seteditor 150 | ``` 151 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setting up local development 2 | - [Fork the project](https://github.com/saurabhdaware/projectman/fork) using fork button in the top right corner. 3 | - Clone the project to your device using `git clone https://github.com/{yourUsername}/projectman.git` 4 | - `npm install` to install the required dependencies 5 | - Type `npm link` to install the content in your global directory. (Your directory will be linked to your global node folder which means if you change anything in the directory it will change inside the global directory as well) 6 | - Type `projectman open` to test your code. 7 | 8 | 9 | ## Directory Structure 10 | There are two important folders that you should care about `lib` and `bin` 11 | ``` 12 | - lib 13 | -> action.js // Contains all the main logic and functions 14 | - bin 15 | -> index.js // Main file, This file is triggered when `projectman` or `pm` is called 16 | ``` 17 | 18 | ## Contribution Guidelines 19 | - If you're planning to implement a new feature I will recommend you to discuss with me before you start coding so you won't end up working on something that I don't want to implement. Create an Issue with proper name and content for discussion. 20 | - If you need any help understanding the code you can reach out to me on twitter/[@saurabhcodes](https://twitter.com/saurabhcodes) 21 | 22 | - For Contributing to this project or any project on GitHub 23 | 1. Fork project. 24 | 2. Create a branch with the name of feature that you're working on (e.g. `multiple-editors`). 25 | 3. Once you're done coding create a merge request from your new branch to my `develop`. (Read the Local Development section above for local setup guidelines) 26 | 27 | 28 | ## Coding Guidelines 29 | - Please write comments wherever necessary. 30 | - Write unit tests wherever possible. 31 | - If you create a new file that exports various functions, put it inside `lib` folder 32 | - Please write proper commit messages explaining your changess. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Saurabh Daware 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProjectMan🦸 2 | 3 |
4 |

5 | 6 | 7 |

8 | 9 | npm 10 | npm bundle size 11 |
12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |

20 | 21 | ProjectMan is a CLI which lets you add projects to favorites using command `pm add` and open them from anywhere you want using command `pm open`. 22 | 23 | Along with this there are also other commands like `pm seteditor`, `pm remove`, `cd $(pm getpath)` mentioned in documentation below. 24 | 25 | --- 26 |

ProjectMan gif explaining how it works

27 | 28 | --- 29 | 30 | ## Installation 31 | 32 | With [NodeJS](https://nodejs.org) installed in your machine, 33 | ```shell 34 | npm install -g projectman 35 | ``` 36 | 37 | --- 38 | 39 | ## Commands 40 | `pm` is an alias of `projectman` so you can use `pm ` or `projectman ` 41 | 42 | ### 📖 Open Project 43 | 44 | Opens project in your code editor (Check out [`pm seteditor`](#set-editor) command to set your preferred editor) 45 | 46 | **Usage :** 47 | ```shell 48 | pm open [projectName] 49 | ``` 50 | `[projectName]` is an optional parameter. 51 | 52 | **Alias:** `pm o`, `pm` 53 | 54 | ### ➕ Add Project or Template 55 | 56 | Add project to favorites 57 | 58 | ```shell 59 | cd /till/the/project 60 | pm add 61 | ``` 62 | 63 | *You can pass `--url` param to add a URL instead of a directory.* 64 | 65 | ### 🪄 Create Project 66 | 67 | Use added projects as a template to create new project 68 | 69 | ```shell 70 | pm create 71 | ``` 72 | 73 | **Alias:** `pm c` 74 | 75 | ### 💫 Set Editor 76 | 77 | ```shell 78 | pm seteditor 79 | ``` 80 | Sets default editor to open projects from. 81 | 82 | To set a different editor for a specific project, 83 | 84 | **Flag:** `--for-project` 85 | ```shell 86 | pm seteditor --for-project 87 | ``` 88 | Sets different editor for a specific project. 89 | E.g You can use VSCode for other projects and Atom for `CoolProject1` 90 | 91 | If your TextEditor/IDE is not listed, You can select option `Other` from the list and give your `editorCommand`. 92 | Read [editorCommand ref](#settings-ref) for more information. 93 | 94 | 95 | ### 🏃🏽 `cd` to a project without opening. 96 | ```shell 97 | cd $(pm getpath [projectName]) 98 | ``` 99 | 100 | `[projectName]` is an optional parameter. 101 | 102 | **Alias :** `cd $(pm gp)` 103 | 104 | (Note: This does not work in Windows cmd, You can use it in Windows Powershell) 105 | 106 | ### ❌ Remove Project 107 | ```shell 108 | pm remove 109 | ``` 110 | Removes project from favorites. 111 | 112 | ### ➖ Remove editor 113 | ```shell 114 | pm rmeditor 115 | ``` 116 | Shows list of project and removes the project specific editor from the project. 117 | 118 | ```shell 119 | pm rmeditor --all 120 | ``` 121 | removes all project specific editors. 122 | 123 | --- 124 | 125 | ## ⚙️ Settings.json 126 | 127 | If you want to sort projects/change name of project/change path, You can type `pm edit` to open settings.json 128 | 129 | 130 | #### Example settings: 131 | ```json 132 | { 133 | "commandToOpen": "code", 134 | "projects": [ 135 | { 136 | "name": "Project1", 137 | "path": "path/to/project1" 138 | }, 139 | { 140 | "name": "Project2", 141 | "path": "path/to/project2", 142 | "editor": "atom" 143 | }, 144 | { 145 | "name": "Project3", 146 | "path": "path/to/project3" 147 | } 148 | ] 149 | } 150 | ``` 151 | This will show three projects in `pm open` and project2 will be opened in Atom and other projects will be opened in Visual Studio Code 152 | 153 | 154 | #### Settings Ref: 155 | 156 | **> commandToOpen** : 157 | - This is your editor's command, this command will be used to open the file in your editor. 158 | - Default is `code` which opens in vscode. 159 | - This is the command that you normally use to open directories in your editor. 160 | 161 | | Editor |'commandToOpen' value| 162 | |---------------|-----------| 163 | | **VSCode** | code | 164 | | **Atom** | atom | 165 | | **Sublime** | subl | 166 | | **Vim** | vim | 167 | | **WebStorm** | wstorm | 168 | 169 | **> projects.`name` :** 170 | - This is the name that will be visible when you type `projectman open` 171 | 172 | **> projects.`path` :** 173 | - This should be the absolute path to your folder. 174 | 175 | **> projects.`editor` :** 176 | - This is optional key. In case it doesn't exist it will read value from `commandToOpen` 177 | - You can use this to specify separate editor for a particular project. 178 | - You can set it by adding `"editor": ""` in projects array in settings.json ([Example](#settingsjson) is shown above) 179 | 180 | --- 181 | 182 | ## ChangeLogs 183 | 184 | ### v2.0.0 *`@latest`* 185 | 186 | - Add `pm create` command 187 | - Remove support for binaries 188 | 189 | 190 | ***For More Changes read [CHANGELOG.md](CHANGELOG.md)*** 191 | 192 | --- 193 | ## Rust Port 194 | [@hskang9](https://github.com/hskang9) has made a pretty cool rust port for projectman. You can check it out at: https://github.com/hskang9/projectman-rust 195 | 196 | --- 197 | ## Contributing to ProjectMan 198 | [![contributions welcome to projectman](https://img.shields.io/badge/contributions-welcome-brightgreen?style=flat-square&logo=github)](https://github.com/saurabhdaware/projectman/issues) 199 | 200 | I would be extremely happy to have people contribute to ProjectMan. You can read Contribution guidelines in **[CONTRIBUTING.md](CONTRIBUTING.md)** 201 | 202 | --- 203 | 204 | **Thank you for showing Interest! Do contribute and star [ProjectMan🦸 on GitHub](https://github.com/saurabhdaware/projectman)** -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: ProjectMan🦸 2 | logo: /images/logo-192.png 3 | description: Hate 'cd'ing to multiple folders to open a certain project? Use ProjectMan to add these projects and open them from command line using single command 'pm open' 4 | theme: jekyll-theme-cayman 5 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const program = require('commander'); 3 | const { suggestCommands } = require('../lib/helper'); 4 | const actions = require('../lib/actions'); 5 | 6 | program.version(require('../package.json').version); 7 | 8 | // Commands 9 | program 10 | .command('open [projectName]') 11 | .alias('o') 12 | .description('Open one of your saved projects') 13 | .action(actions.openProject); 14 | 15 | program 16 | .command('create [projectName]') 17 | .alias('c') 18 | .description('Create project') 19 | .action(actions.createProject); 20 | 21 | program 22 | .command('add [projectDirectory]') 23 | .alias('save') 24 | .option('-u, --url [link]', 'Add a link to a repository to projects') 25 | .description('Save current directory as a project') 26 | .action(actions.addProject); 27 | 28 | program 29 | .command('remove [projectName]') 30 | .description('Remove the project') 31 | .action(actions.removeProject); 32 | 33 | program 34 | .command('seteditor [commandToOpen]') 35 | .description('Set text editor to use') 36 | .option( 37 | '-f|--for-project [projectName]', 38 | 'set different editor for specific project' 39 | ) 40 | .action(actions.setEditor); 41 | 42 | program 43 | .command('rmeditor [projectName]') 44 | .description('Remove text editor to use') 45 | .option('-a|--all', 'remove editors from all projects') 46 | .action(actions.rmEditor); 47 | 48 | program 49 | .command('edit') 50 | .description('Edit settings.json') 51 | .action(actions.editConfigurations); 52 | 53 | program 54 | .command('getpath [projectName]') 55 | .alias('gp') 56 | .description('Get project path') 57 | .action(actions.getProjectPath); 58 | 59 | program.arguments('').action((command) => { 60 | console.log(`Command ${command} not found\n`); 61 | program.outputHelp(); 62 | suggestCommands(command); 63 | }); 64 | 65 | program.usage(''); 66 | 67 | if (process.argv.length <= 2) { 68 | // If no command mentioned then output help 69 | actions.openProject(); 70 | } 71 | 72 | // Parse arguments 73 | program.parse(process.argv); 74 | -------------------------------------------------------------------------------- /images/logo-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saurabhdaware/projectman/81eb4d206d0822adcf3562d76f00055e60239c9b/images/logo-192.png -------------------------------------------------------------------------------- /images/logo-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saurabhdaware/projectman/81eb4d206d0822adcf3562d76f00055e60239c9b/images/logo-512.png -------------------------------------------------------------------------------- /images/terminal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saurabhdaware/projectman/81eb4d206d0822adcf3562d76f00055e60239c9b/images/terminal.gif -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | 3 | const openProject = require('./commands/open'); 4 | const addProject = require('./commands/add'); 5 | const removeProject = require('./commands/remove'); 6 | const editConfigurations = require('./commands/edit'); 7 | const setEditor = require('./commands/seteditor'); 8 | const rmEditor = require('./commands/rmeditor'); 9 | const getProjectPath = require('./commands/getpath'); 10 | const createProject = require('./commands/create'); 11 | 12 | module.exports = { 13 | openProject, 14 | addProject, 15 | removeProject, 16 | editConfigurations, 17 | setEditor, 18 | rmEditor, 19 | getProjectPath, 20 | createProject 21 | }; 22 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | // Just some weird strings that color text inside it. You probably will not have to touch this file. 2 | 3 | exports.green = (message) => `\u001b[32m${message}\u001b[39m`; 4 | 5 | exports.boldGreen = (message) => 6 | `\u001b[1m\u001b[32m${message}\u001b[39m\u001b[22m`; 7 | 8 | exports.boldRed = (message) => 9 | `\u001b[1m\u001b[31m${message}\u001b[39m\u001b[22m`; 10 | 11 | exports.yellow = (message) => `\u001b[33m${message}\u001b[39m`; 12 | 13 | exports.boldYellow = (message) => 14 | `\u001b[1m\u001b[33m${message}\u001b[39m\u001b[22m`; 15 | 16 | exports.grey = (message) => `\u001b[90m${message}\u001b[39m`; 17 | 18 | exports.boldGrey = (message) => 19 | `\u001b[1m\u001b[90m${message}\u001b[39m\u001b[22m`; 20 | 21 | exports.bold = (message) => `\u001b[1m${message}\u001b[22m`; 22 | 23 | exports.boldBlue = (message) => this.bold(`\u001b[34m${message}\u001b[39m`); 24 | -------------------------------------------------------------------------------- /lib/commands/add.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | const prompts = require('prompts'); 3 | 4 | // internal modules 5 | const path = require('path'); 6 | 7 | // helper functions 8 | const color = require('../colors.js'); 9 | const logs = require('../logs.js'); 10 | 11 | const { getSettings, writeSettings, isURL, onCancel } = require('../helper.js'); 12 | 13 | /* 14 | 15 | COMMAND: pm add [projectDirectory] [--url [link]] 16 | ARGUMENTS: 17 | [projectDirectory] :: Directory of the project that you want to add. OR when --url flag is added, URL of the repository. 18 | 19 | FLAG: 20 | [--url [link]] :: Adds URL. 21 | 22 | RETURNS: newProject object. 23 | 24 | */ 25 | async function addProject(projectDirectory = '.', cmdObj = undefined) { 26 | const settings = getSettings(); 27 | const newProject = {}; 28 | let name; 29 | let enteredUrl; 30 | 31 | if (cmdObj.url) { 32 | if (projectDirectory !== '.') { 33 | console.warn( 34 | "Project's local directory value will be ignore when --url flag is on" 35 | ); 36 | } 37 | 38 | if (cmdObj.url == true) { 39 | ({ enteredUrl } = await prompts( 40 | [ 41 | { 42 | type: 'text', 43 | message: 'Project URL :', 44 | name: 'enteredUrl', 45 | initial: 'https://github.com/', 46 | validate: (url) => (isURL(url) ? true : 'Not a valid URL') 47 | } 48 | ], 49 | { onCancel } 50 | )); 51 | name = enteredUrl.split('/').pop(); // Get last route of URL to set default name 52 | newProject.path = enteredUrl; 53 | } else { 54 | if (!isURL(cmdObj.url)) { 55 | logs.error('Not a valid URL'); 56 | console.warn( 57 | 'A valid URL looks something like ' + 58 | color.yellow('https://github.com/saurabhdaware/projectman') 59 | ); 60 | return; 61 | } 62 | name = cmdObj.url.split('/').pop(); // Get last route of URL to set default name 63 | newProject.path = cmdObj.url; 64 | } 65 | } else { 66 | newProject.path = path.resolve(projectDirectory); 67 | name = newProject.path.split(path.sep).pop(); 68 | } 69 | 70 | ({ finalName: newProject.name } = await prompts( 71 | [ 72 | { 73 | type: 'text', 74 | message: 'Project Name :', 75 | name: 'finalName', 76 | initial: name 77 | } 78 | ], 79 | { onCancel } 80 | )); 81 | 82 | if ( 83 | settings.projects.some( 84 | (project) => project.name.toLowerCase() == newProject.name.toLowerCase() 85 | ) 86 | ) { 87 | logs.error('Project with this name already exists'); 88 | return; 89 | } 90 | 91 | settings.projects.push(newProject); 92 | 93 | writeSettings(settings, 'add', 'Project Added'); 94 | 95 | return newProject; 96 | } 97 | 98 | module.exports = addProject; 99 | -------------------------------------------------------------------------------- /lib/commands/create.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | const prompts = require('prompts'); 3 | const gitIgnoreParser = require('gitignore-parser'); 4 | 5 | // internal modules 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const spawn = require('child_process').spawn; 9 | 10 | // helper functions 11 | const color = require('../colors.js'); 12 | const logs = require('../logs.js'); 13 | 14 | const { 15 | onCancel, 16 | selectProject, 17 | copyFolderSync, 18 | getSettings, 19 | isURL, 20 | logger 21 | } = require('../helper.js'); 22 | 23 | // pm create [projectName] 24 | async function createProject(projectName) { 25 | const settings = getSettings(); 26 | if (settings.projects.length == 0) { 27 | logs.error('No projects to open :('); 28 | console.warn( 29 | `cd /till/project/directory/ and run ${color.boldYellow( 30 | 'pm add' 31 | )} to add projects and get started` 32 | ); 33 | return; 34 | } 35 | 36 | const selectedTemplate = await selectProject( 37 | undefined, 38 | 'Select starting template' 39 | ); 40 | 41 | if (!selectedTemplate) { 42 | logs.error( 43 | `Project does not exist. Add it using ${color.yellow( 44 | 'pm add [projectPath]' 45 | )} or cd to the project folder and type ${color.yellow('pm add')}` 46 | ); 47 | return; 48 | } 49 | 50 | if (!projectName) { 51 | ({ projectName } = await prompts( 52 | [ 53 | { 54 | type: 'text', 55 | message: 'Project Name:', 56 | name: 'projectName', 57 | initial: '', 58 | validate: (val) => { 59 | if (!val) { 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | } 66 | ], 67 | { onCancel } 68 | )); 69 | } 70 | 71 | const newProjectDirectoryName = projectName.toLowerCase().replace(/ /g, '-'); 72 | const newProjectDirectory = path.join(process.cwd(), newProjectDirectoryName); 73 | 74 | if (isURL(selectedTemplate.path)) { 75 | if (!selectedTemplate.path.includes('github.com')) { 76 | console.error( 77 | 'Currently only github.com URLs can be used to scaffold project' 78 | ); 79 | return; 80 | } 81 | 82 | const childProcess = spawn( 83 | 'git', 84 | ['clone', selectedTemplate.path, newProjectDirectory], 85 | { 86 | stdio: 'inherit' 87 | } 88 | ); 89 | 90 | childProcess.on('exit', (code) => { 91 | if (code === 0) { 92 | console.log(''); 93 | logger.success( 94 | // eslint-disable-next-line max-len 95 | `${newProjectDirectoryName} is successfully scaffolded from ${selectedTemplate.path}` 96 | ); 97 | } else { 98 | console.log(''); 99 | logger.error( 100 | // eslint-disable-next-line max-len 101 | `Could not scaffold project. Git clone exitted with 1. Check error above` 102 | ); 103 | } 104 | }); 105 | 106 | return; 107 | } 108 | 109 | let gitignoreContent = '.git/\n'; 110 | const gitignorePath = path.join(selectedTemplate.path, '.gitignore'); 111 | 112 | if (fs.existsSync(gitignorePath)) { 113 | gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8'); 114 | } 115 | 116 | const gitignore = gitIgnoreParser.compile(gitignoreContent); 117 | copyFolderSync( 118 | selectedTemplate.path, 119 | newProjectDirectory, 120 | gitignore, 121 | true, 122 | selectedTemplate.path 123 | ); 124 | } 125 | 126 | module.exports = createProject; 127 | -------------------------------------------------------------------------------- /lib/commands/edit.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const exec = util.promisify(require('child_process').exec); 3 | 4 | // helper functions 5 | const color = require('../colors.js'); 6 | const logs = require('../logs.js'); 7 | 8 | const { SETTINGS_PATH, throwCreateIssueError } = require('../helper.js'); 9 | 10 | // projectman edit 11 | async function editConfigurations() { 12 | let openSettingsCommand; 13 | 14 | if (process.platform == 'win32') { 15 | openSettingsCommand = 'Notepad '; 16 | } else if (process.platform == 'linux') { 17 | openSettingsCommand = 'xdg-open '; 18 | } else { 19 | openSettingsCommand = 'open -t '; 20 | } 21 | 22 | try { 23 | const { stderr } = await exec(`${openSettingsCommand} "${SETTINGS_PATH}"`); 24 | if (stderr) { 25 | logs.error('Error occured while opening the file: ' + SETTINGS_PATH); 26 | console.log('You can follow above path and manually edit settings.json'); 27 | throwCreateIssueError(stderr); 28 | return; 29 | } 30 | 31 | console.log( 32 | color.boldGreen('>>> ') + 'Opening settings.json' + color.green(' ✔') 33 | ); 34 | } catch (err) { 35 | logs.error('Error occured while opening the file: ' + SETTINGS_PATH); 36 | console.warn('You can follow above path and manually edit settings.json'); 37 | throwCreateIssueError(err); 38 | } 39 | } 40 | 41 | module.exports = editConfigurations; 42 | -------------------------------------------------------------------------------- /lib/commands/getpath.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | const prompts = require('prompts'); 3 | 4 | // helper functions 5 | const color = require('../colors.js'); 6 | const logs = require('../logs.js'); 7 | 8 | const { onCancel, getChoices, getSettings } = require('../helper.js'); 9 | 10 | async function getProjectPath(projectName) { 11 | const settings = getSettings(); 12 | let selectedProject; 13 | if (settings.projects.length === 0) { 14 | logs.error('No projects to get path :('); 15 | console.warn( 16 | `cd /till/project/directory/ and run ${color.boldYellow( 17 | 'pm add' 18 | )} to add projects and get started` 19 | ); 20 | return; 21 | } 22 | 23 | if (!projectName) { 24 | const question = { 25 | name: 'selectedProject', 26 | message: 'Select project you want to cd to:', 27 | type: 'autocomplete', 28 | stdout: process.stderr, 29 | choices: getChoices(), 30 | onRender: () => { 31 | process.stderr.write('\033c'); 32 | } 33 | }; 34 | 35 | ({ selectedProject } = await prompts([question], { onCancel })); 36 | if (!selectedProject) { 37 | logs.error( 38 | `Project does not exist. Add it using ${color.yellow( 39 | 'pm add [projectPath]' 40 | )} or cd till the project folder and type ${color.yellow('pm add')}` 41 | ); 42 | return; 43 | } 44 | } else { 45 | selectedProject = settings.projects.find( 46 | (project) => project.name.toLowerCase() == projectName.toLowerCase() 47 | ); 48 | } 49 | 50 | // Print path 51 | console.log(selectedProject.path); 52 | } 53 | 54 | module.exports = getProjectPath; 55 | -------------------------------------------------------------------------------- /lib/commands/open.js: -------------------------------------------------------------------------------- 1 | // internal modules 2 | const util = require('util'); 3 | const exec = util.promisify(require('child_process').exec); 4 | 5 | // helper functions 6 | const color = require('../colors.js'); 7 | const logs = require('../logs.js'); 8 | 9 | const { 10 | getSettings, 11 | throwCreateIssueError, 12 | openURL, 13 | isURL, 14 | selectProject 15 | } = require('../helper.js'); 16 | 17 | /** 18 | * COMMAND: pm open [projectName] 19 | * 20 | * ARGUMENTS: [projectName] :: Name of the project to open 21 | * 22 | * @param {string} projectName - Name of the project to open 23 | */ 24 | async function openProject(projectName) { 25 | const settings = getSettings(); 26 | if (settings.projects.length == 0) { 27 | logs.noProjectsToOpen(); 28 | return; 29 | } 30 | 31 | console.log( 32 | color.boldGrey('>>> Default editor: ') + color.grey(settings.commandToOpen) 33 | ); 34 | const selectedProject = await selectProject( 35 | projectName, 36 | 'Select project to open' 37 | ); 38 | 39 | if (!selectedProject) { 40 | logs.projectDoesNotExist(); 41 | return; 42 | } 43 | 44 | const commandToOpen = selectedProject.editor || settings.commandToOpen; // If project specific editor exists, Then open using that command else use global command 45 | let stderr; 46 | try { 47 | if (isURL(selectedProject.path)) { 48 | ({ stderr } = await openURL(selectedProject.path)); // If it is URL then open in Browser. 49 | } else { 50 | ({ stderr } = await exec(`${commandToOpen} "${selectedProject.path}"`)); // This line opens projects 51 | } 52 | 53 | if (stderr) { 54 | logs.error('Could not open project for some reason :('); 55 | throwCreateIssueError(stderr); 56 | return; 57 | } 58 | 59 | console.log( 60 | `${color.boldGreen('>>>')} Opening ${selectedProject.name} ${color.green( 61 | '✔' 62 | )}` 63 | ); // Success yay! 64 | } catch (err) { 65 | logs.error('Could not open project :('); // Something broke :( 66 | console.warn( 67 | `Are you sure your editor uses command ${color.yellow( 68 | commandToOpen 69 | )} to open directories from terminal?` 70 | ); 71 | console.warn( 72 | `If not, use ${color.yellow( 73 | 'pm seteditor' 74 | )} to set Editor/IDE of your choice` 75 | ); 76 | throwCreateIssueError(err); 77 | return; 78 | } 79 | } 80 | 81 | module.exports = openProject; 82 | -------------------------------------------------------------------------------- /lib/commands/remove.js: -------------------------------------------------------------------------------- 1 | // helper functions 2 | const color = require('../colors.js'); 3 | const logs = require('../logs.js'); 4 | 5 | const { getSettings, writeSettings, selectProject } = require('../helper.js'); 6 | 7 | // pm remove [projectName] 8 | async function removeProject(projectName) { 9 | const settings = getSettings(); 10 | 11 | const { name: selectedProjectName } = await selectProject( 12 | projectName, 13 | 'Select project to remove' 14 | ); 15 | 16 | if (!selectedProjectName) { 17 | logs.error(`Project with name ${selectedProjectName} does not exist.`); 18 | console.log( 19 | `Try ${color.yellow( 20 | 'pm remove' 21 | )} and select the project you want to remove` 22 | ); 23 | return; 24 | } 25 | // removing project 26 | settings.projects = settings.projects.filter( 27 | (project) => 28 | project.name.toLowerCase() !== selectedProjectName.toLowerCase() 29 | ); 30 | 31 | writeSettings(settings, 'remove', 'Project Removed'); 32 | } 33 | 34 | module.exports = removeProject; 35 | -------------------------------------------------------------------------------- /lib/commands/rmeditor.js: -------------------------------------------------------------------------------- 1 | const color = require('../colors.js'); 2 | const logs = require('../logs.js'); 3 | 4 | const { getSettings, writeSettings, selectProject } = require('../helper.js'); 5 | 6 | async function rmEditor(projectName, cmdObj) { 7 | const settings = getSettings(); 8 | 9 | let newSettings; 10 | if (cmdObj.all) { 11 | newSettings = { 12 | ...settings, 13 | projects: settings.projects.map(({ ...project }) => { 14 | if (project.editor) delete project.editor; 15 | return project; 16 | }) 17 | }; 18 | } else { 19 | console.log( 20 | color.boldGrey('>>> Default editor: ') + 21 | color.grey(settings.commandToOpen) 22 | ); 23 | const selectedProject = await selectProject( 24 | projectName, 25 | 'Select project to remove editor from' 26 | ); 27 | if (!selectedProject) { 28 | logs.error(`Project with name ${selectedProject} does not exist.`); 29 | console.log( 30 | `Try ${color.yellow( 31 | 'pm rmeditor' 32 | )} and select the project you want to remove the editor from` 33 | ); 34 | return; 35 | } 36 | 37 | const selectedIndex = settings.projects.findIndex( 38 | (project) => 39 | selectedProject.name.toLowerCase() == project.name.toLowerCase() 40 | ); 41 | delete settings.projects[selectedIndex].editor; 42 | newSettings = { ...settings }; 43 | } 44 | 45 | writeSettings(newSettings, 'rmeditor', `TextEditor Removed`); 46 | } 47 | 48 | module.exports = rmEditor; 49 | -------------------------------------------------------------------------------- /lib/commands/seteditor.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | const prompts = require('prompts'); 3 | 4 | // helper functions 5 | const color = require('../colors.js'); 6 | 7 | const { 8 | getSettings, 9 | writeSettings, 10 | isURL, 11 | onCancel, 12 | selectProject 13 | } = require('../helper.js'); 14 | 15 | const questions = [ 16 | { 17 | type: 'select', 18 | message: 'Select Project Open Command', 19 | name: 'selectedEditor', 20 | choices: [ 21 | { 22 | title: 'code', 23 | value: 'code' 24 | }, 25 | { 26 | title: 'cursor', 27 | value: 'cursor' 28 | }, 29 | { 30 | title: 'subl', 31 | value: 'subl' 32 | }, 33 | { 34 | title: 'atom', 35 | value: 'atom' 36 | }, 37 | { 38 | title: 'vim', 39 | value: 'vim' 40 | }, 41 | { 42 | title: 'Other', 43 | value: 'other' 44 | } 45 | ] 46 | } 47 | ]; 48 | 49 | // pm seteditor [command] 50 | async function setEditor(command, cmdObj = undefined) { 51 | let commandToOpen; 52 | let selectedProject; 53 | let selectedIndex = -1; // this will have the index of selectedProject 54 | let projectName; 55 | const settings = getSettings(); 56 | 57 | const setEditorFilter = (project) => !isURL(project.path); 58 | 59 | if (cmdObj.forProject) { 60 | // --for-project exists 61 | projectName = cmdObj.forProject === true ? undefined : cmdObj.forProject; // if only[--for-project], set undefined else if [--for-project [projectName]] set projectName 62 | selectedProject = await selectProject( 63 | projectName, 64 | 'Select project to set editor for', 65 | setEditorFilter 66 | ); // sending undefined, calls list of projects to select from 67 | // find the index of selectedProject and add editor to that project 68 | selectedIndex = settings.projects.findIndex( 69 | (project) => 70 | project.name.toLowerCase() == selectedProject.name.toLowerCase() 71 | ); 72 | } 73 | 74 | if (!command) { 75 | const { selectedEditor } = await prompts(questions, { onCancel }); 76 | if (selectedEditor == 'other') { 77 | console.warn('Enter command that you use to open Editor from Terminal'); 78 | console.log( 79 | `E.g With VSCode Installed, you can type ${color.yellow( 80 | 'code ' 81 | )} in terminal to open directory` 82 | ); 83 | console.log( 84 | `In this case, the command would be ${color.yellow('code')}\n` 85 | ); 86 | const question = { 87 | type: 'text', 88 | message: 'Enter command :', 89 | name: 'command', 90 | validate: function (val) { 91 | return val !== ''; 92 | } 93 | }; 94 | const { command } = await prompts([question], { onCancel }); 95 | commandToOpen = command; 96 | } else { 97 | commandToOpen = selectedEditor; 98 | } 99 | } else { 100 | commandToOpen = command; 101 | } 102 | 103 | if (selectedIndex < 0) { 104 | settings.commandToOpen = commandToOpen; 105 | } else { 106 | settings.projects[selectedIndex].editor = commandToOpen; 107 | } 108 | 109 | writeSettings( 110 | settings, 111 | 'seteditor', 112 | `Text Editor Selected ${ 113 | selectedIndex < 0 ? '' : `for ${settings.projects[selectedIndex].name}` 114 | }` 115 | ); 116 | } 117 | 118 | module.exports = setEditor; 119 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | // external dependencies 2 | const prompts = require('prompts'); 3 | const program = require('commander'); 4 | const didYouMean = require('didyoumean'); 5 | 6 | // internal modules 7 | const path = require('path'); 8 | const fs = require('fs'); 9 | const util = require('util'); 10 | const exec = util.promisify(require('child_process').exec); 11 | const os = require('os'); 12 | 13 | // helper functions 14 | const color = require('./colors.js'); 15 | 16 | // Default settings. 17 | const DEFAULT_SETTINGS = { 18 | commandToOpen: 19 | process.platform === 'darwin' 20 | ? 'open -a "/Applications/Visual Studio Code.app"' 21 | : 'code', 22 | projects: [] 23 | }; 24 | 25 | // Create settings.json if does not exist or just require it if it does exist 26 | const SETTINGS_DIR = 27 | process.env.NODE_ENV === 'test' 28 | ? path.join(__dirname, '..', 'tests', 'dotprojectman') 29 | : path.join(os.homedir(), '.projectman'); 30 | 31 | const SETTINGS_PATH = path.join(SETTINGS_DIR, 'settings.json'); 32 | 33 | /** 34 | * Returns settings if already exists, else creates default settings and returns it 35 | * @returns {import('../types/utils').SettingsType} 36 | */ 37 | const getSettings = () => { 38 | let settings; 39 | try { 40 | settings = require(SETTINGS_PATH); 41 | } catch (err) { 42 | if (err.code === 'MODULE_NOT_FOUND') { 43 | // Create if doesn't exist 44 | if (!fs.existsSync(SETTINGS_DIR)) { 45 | fs.mkdirSync(SETTINGS_DIR); 46 | } 47 | fs.writeFileSync( 48 | SETTINGS_PATH, 49 | JSON.stringify(DEFAULT_SETTINGS, null, 4), 50 | 'utf8' 51 | ); 52 | settings = DEFAULT_SETTINGS; 53 | } 54 | } 55 | 56 | return settings; 57 | }; 58 | // 59 | 60 | const settings = getSettings(); 61 | 62 | const logger = { 63 | error: (message) => console.log(color.boldRed('>>> ') + message), 64 | warn: (message) => console.log(color.boldYellow('>>> ') + message), 65 | success: (message) => console.log(color.boldGreen('>>> ') + message + ' ✔') 66 | }; 67 | 68 | function throwCreateIssueError(err) { 69 | logger.error( 70 | // eslint-disable-next-line max-len 71 | 'If you think it is my fault please create issue at https://github.com/saurabhdaware/projectman with below log' 72 | ); 73 | console.log(color.boldRed('Err:')); 74 | console.log(err); 75 | } 76 | 77 | // Takes data as input and writes that data to settings.json 78 | function writeSettings( 79 | data, 80 | command = '', 81 | successMessage = 'Settings updated :D' 82 | ) { 83 | fs.writeFile(SETTINGS_PATH, JSON.stringify(data, null, 2), (err) => { 84 | if (err) { 85 | if (err.code === 'EACCES') { 86 | const errCmd = 87 | process.platform == 'win32' 88 | ? `an admin` 89 | : `a super user ${color.boldYellow(`sudo pm ${command}`)}`; 90 | logger.error(`Access Denied! please try again as ${errCmd}`); 91 | return; 92 | } 93 | throwCreateIssueError(err); 94 | return; 95 | } 96 | logger.success(successMessage); 97 | }); 98 | } 99 | 100 | async function openURL(url) { 101 | let stderr; 102 | switch (process.platform) { 103 | case 'darwin': 104 | ({ stderr } = await exec(`open ${url}`)); 105 | break; 106 | case 'win32': 107 | ({ stderr } = await exec(`start ${url}`)); 108 | break; 109 | default: 110 | ({ stderr } = await exec(`xdg-open ${url}`)); 111 | break; 112 | } 113 | if (stderr) { 114 | console.log(stderr); 115 | } 116 | return stderr; 117 | } 118 | 119 | function isURL(str) { 120 | // eslint-disable-next-line max-len 121 | const regex = /(http|https):\/\/(\w+:{0,1}\w*)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%!\-\/]))?/; 122 | if (!regex.test(str)) { 123 | return false; 124 | } else { 125 | return true; 126 | } 127 | } 128 | 129 | const suggestFilter = (input, choices) => { 130 | return Promise.resolve( 131 | choices.filter((choice) => 132 | choice.title.toLowerCase().includes(input.toLowerCase()) 133 | ) 134 | ); 135 | }; 136 | 137 | const suggestCommands = (cmd) => { 138 | const suggestion = didYouMean( 139 | cmd, 140 | program.commands.map((cmd) => cmd._name) 141 | ); 142 | if (suggestion) { 143 | console.log(); 144 | console.log(`Did you mean ${suggestion}?`); 145 | } 146 | }; 147 | 148 | const onCancel = () => { 149 | logger.error("See ya ('__') /"); 150 | process.exit(); 151 | return false; 152 | }; 153 | 154 | function getChoices(customFilter = () => true) { 155 | const projects = [...settings.projects]; 156 | const result = projects.filter(customFilter).map(({ ...project }) => { 157 | // Spreading project to make it immutable 158 | project.title = project.name; 159 | project.value = { name: project.name, path: project.path }; 160 | if (project.editor && project.editor !== settings.commandToOpen) { 161 | project.title += color.grey( 162 | ` (${color.boldGrey('editor:')} ${color.grey(project.editor + ')')}` 163 | ); 164 | project.value.editor = project.editor; 165 | } 166 | 167 | if (isURL(project.path)) { 168 | project.title += color.boldGrey(` (URL)`); 169 | project.description = project.path; 170 | } 171 | 172 | return project; 173 | }); 174 | 175 | return result; 176 | } 177 | 178 | async function selectProject(projectName, message, customFilter = () => true) { 179 | let selectedProject; 180 | if (!projectName) { 181 | // Ask which project they want to open 182 | const questions = [ 183 | { 184 | type: 'autocomplete', 185 | message: `${message}: `, 186 | name: 'selectedProject', 187 | choices: getChoices(customFilter), 188 | limit: 40, 189 | suggest: suggestFilter 190 | } 191 | ]; 192 | 193 | // Redirecting to stderr in order for it to be used with command substitution 194 | ({ selectedProject } = await prompts(questions, { onCancel })); 195 | } else { 196 | // If project name is mentioned then open directly 197 | selectedProject = settings.projects.find( 198 | (project) => project.name.toLowerCase() == projectName.toLowerCase() 199 | ); 200 | } 201 | return selectedProject; 202 | } 203 | 204 | /** 205 | * Recursively creates the path 206 | * @param {String} pathToCreate path that you want to create 207 | */ 208 | function createPathIfAbsent(pathToCreate) { 209 | // prettier-ignore 210 | pathToCreate 211 | .split(path.sep) 212 | .reduce((prevPath, folder) => { 213 | const currentPath = path.join(prevPath, folder, path.sep); 214 | if (!fs.existsSync(currentPath)) { 215 | fs.mkdirSync(currentPath); 216 | } 217 | return currentPath; 218 | }, ''); 219 | } 220 | 221 | // const isDirectoryIgnored = (ignoredPatterns, filePath) => { 222 | // const ignoredDir = ignoredPatterns.find((pattern) => 223 | // minimatch(filePath, pattern) 224 | // ); 225 | 226 | // if (ignoredDir) { 227 | // console.log('>> ignoring ', filePath); 228 | // } 229 | 230 | // return !!ignoredDir; 231 | // }; 232 | 233 | const isGitIgnored = (filePath, basePath, gitignore) => { 234 | return gitignore.denies(path.relative(basePath, filePath)); 235 | }; 236 | 237 | /** 238 | * 239 | * @param {String} from - Path to copy from 240 | * @param {String} to - Path to copy to 241 | * @param {Array} ignore - files/directories to ignore 242 | * @param {Boolean} ignoreEmptyDirs - Ignore empty directories while copying 243 | * @return {void} 244 | */ 245 | function copyFolderSync(from, to, gitignore, ignoreEmptyDirs = true, basePath) { 246 | if (isGitIgnored(from, basePath, gitignore)) { 247 | return; 248 | } 249 | const fromDirectories = fs.readdirSync(from); 250 | 251 | createPathIfAbsent(to); 252 | fromDirectories.forEach((element) => { 253 | const fromElement = path.join(from, element); 254 | const toElement = path.join(to, element); 255 | if (fs.lstatSync(fromElement).isFile()) { 256 | if (!isGitIgnored(fromElement, basePath, gitignore)) { 257 | fs.copyFileSync(fromElement, toElement); 258 | } 259 | } else { 260 | copyFolderSync( 261 | fromElement, 262 | toElement, 263 | gitignore, 264 | ignoreEmptyDirs, 265 | basePath 266 | ); 267 | if (fs.existsSync(toElement) && ignoreEmptyDirs) { 268 | try { 269 | fs.rmdirSync(toElement); 270 | } catch (err) { 271 | if (err.code !== 'ENOTEMPTY') throw err; 272 | } 273 | } 274 | } 275 | }); 276 | } 277 | 278 | // Helper funcitions [END] 279 | 280 | module.exports = { 281 | settings, 282 | logger, 283 | getSettings, 284 | SETTINGS_PATH, 285 | throwCreateIssueError, 286 | writeSettings, 287 | openURL, 288 | isURL, 289 | suggestCommands, 290 | onCancel, 291 | getChoices, 292 | selectProject, 293 | copyFolderSync 294 | }; 295 | -------------------------------------------------------------------------------- /lib/logs.js: -------------------------------------------------------------------------------- 1 | const color = require('./colors'); 2 | const { throwCreateIssueError } = require('./helper'); 3 | 4 | const error = (message) => { 5 | console.error(color.boldRed('>>>'), message); 6 | }; 7 | 8 | const warn = (message) => { 9 | console.warn(color.boldYellow('>>>'), message); 10 | }; 11 | 12 | const info = (message) => { 13 | console.log(color.boldBlue('>>>'), message); 14 | }; 15 | 16 | const noProjectsToOpen = () => { 17 | error('No projects to open :('); 18 | info( 19 | `cd /till/project/directory/ and run ${color.yellow( 20 | 'pm add' 21 | )} to add projects and get started` 22 | ); 23 | }; 24 | 25 | const projectDoesNotExist = () => { 26 | error( 27 | `Project does not exist. Add it using ${color.yellow( 28 | 'pm add [projectPath]' 29 | )} or cd till the project folder and type ${color.yellow('pm add')}` 30 | ); 31 | }; 32 | 33 | const unknownError = (stderr) => { 34 | error('Something went wrong :('); 35 | throwCreateIssueError(stderr); 36 | }; 37 | 38 | module.exports = { 39 | noProjectsToOpen, 40 | projectDoesNotExist, 41 | unknownError, 42 | error, 43 | warn 44 | }; 45 | -------------------------------------------------------------------------------- /mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "diff": true, 3 | "extension": ["js"], 4 | "opts": false, 5 | "package": "./package.json", 6 | "reporter": "spec", 7 | "slow": 75, 8 | "timeout": 2000, 9 | "ui": "bdd" 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "projectman", 3 | "version": "2.2.0", 4 | "description": "Hate opening folders? Select and open your projects in your favourite editor straight from your command line without 'CD'ing into the deeply nested folders.", 5 | "main": "bin/index.js", 6 | "bin": { 7 | "projectman": "bin/index.js", 8 | "pm": "bin/index.js" 9 | }, 10 | "preferGlobal": true, 11 | "scripts": { 12 | "test": "jest", 13 | "build": "pkg ./package.json --out-path ./dist || echo '\nPlease first `npm install -g pkg` to run this command'", 14 | "eslint": "eslint lib bin", 15 | "prettier": "prettier lib bin --write" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/saurabhdaware/projectman.git" 20 | }, 21 | "keywords": [ 22 | "project-manager", 23 | "projectman", 24 | "open-projects", 25 | "projectmanager", 26 | "congifure-projects" 27 | ], 28 | "author": "Saurabh Daware (https://saurabhdaware.in)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/saurabhdaware/projectman/issues" 32 | }, 33 | "homepage": "https://github.com/saurabhdaware/projectman#readme", 34 | "dependencies": { 35 | "commander": "^3.0.1", 36 | "didyoumean": "^1.2.1", 37 | "gitignore-parser": "^0.0.2", 38 | "prompts": "^2.3.0" 39 | }, 40 | "devDependencies": { 41 | "cli-testing-tool": "^0.2.0", 42 | "eslint": "^7.0.0", 43 | "eslint-config-google": "^0.14.0", 44 | "eslint-config-prettier": "^6.11.0", 45 | "eslint-plugin-prettier": "^3.1.3", 46 | "jest": "^27.2.0", 47 | "prettier": "^2.0.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/action.spec.js: -------------------------------------------------------------------------------- 1 | describe('action', () => { 2 | test('this test should be replaced with real tests', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/helper.spec.js: -------------------------------------------------------------------------------- 1 | const { isURL } = require('../lib/helper'); 2 | const cleanUp = require('./utils/cleanup'); 3 | 4 | describe('helper', () => { 5 | afterEach(cleanUp); 6 | test('#isURL()', () => { 7 | // Links 8 | expect(isURL('https://www.google.com')).toBe(true); 9 | expect(isURL('https://www.data.gov.in')).toBe(true); 10 | expect(isURL('http://facebook.com')).toBe(true); 11 | expect(isURL('http://github.com')).toBe(true); 12 | 13 | // Not Links 14 | expect(isURL('helloworld')).toBe(false); 15 | expect(isURL('htts://www.google.com')).toBe(false); 16 | expect(isURL('hats://facebook')).toBe(false); 17 | expect(isURL('blablabla.com')).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/open.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createCommandInterface, parseOutput } = require('cli-testing-tool'); 3 | const cleanUp = require('./utils/cleanup'); 4 | const { STRING_ESC } = require('cli-testing-tool/lib/cli-ansi-parser'); 5 | 6 | const NO_PROJECT_FOUND_ERROR = 7 | '[BOLD_START][RED_START]>>>[COLOR_END][BOLD_END] No projects to open :('; 8 | const CD_TO_ADD_PROJECT_MSG = 9 | // eslint-disable-next-line max-len 10 | '[BOLD_START][BLUE_START]>>>[COLOR_END][BOLD_END] cd /till/project/directory/ and run [YELLOW_START]pm add[COLOR_END] to add projects and get started'; 11 | 12 | async function addProject(dir) { 13 | const addCommandInterface = createCommandInterface( 14 | `node ${__dirname}/../bin/index.js add`, 15 | { 16 | cwd: dir 17 | } 18 | ); 19 | await addCommandInterface.keys.enter(); // selects default name 20 | return addCommandInterface.getOutput(); 21 | } 22 | 23 | describe('projectman open', () => { 24 | afterEach(() => { 25 | cleanUp(); 26 | }); 27 | 28 | test('should log no error found and pm add instructions', async () => { 29 | const commandInterface = createCommandInterface( 30 | 'node ../bin/index.js open', 31 | { 32 | cwd: __dirname 33 | } 34 | ); 35 | 36 | const terminal = await commandInterface.getOutput(); 37 | expect(terminal.tokenizedOutput).toBe( 38 | NO_PROJECT_FOUND_ERROR + '\n' + CD_TO_ADD_PROJECT_MSG 39 | ); 40 | }); 41 | 42 | // eslint-disable-next-line max-len 43 | test('should show dropdown with tests and utils and with arrow on utils', async () => { 44 | // add tests and utils 45 | await addProject(__dirname); 46 | await addProject(path.join(__dirname, 'utils')); 47 | 48 | const openCommandInterface = createCommandInterface( 49 | 'node ../bin/index.js open', 50 | { 51 | cwd: __dirname 52 | } 53 | ); 54 | await openCommandInterface.keys.arrowDown(); // move down to select utils 55 | const { rawOutput } = await openCommandInterface.getOutput(); 56 | /** 57 | * Why slice from last clear line? 58 | * 59 | * Even though we see one output in terminal, sometimes libraries can create multiple outputs 60 | * slicing from the last clear line just makes sure we only get final output. 61 | */ 62 | const outputAfterLastLineClear = rawOutput.slice( 63 | rawOutput.lastIndexOf(`${STRING_ESC}2K`) 64 | ); 65 | const parsedOutputAfterLastLineClear = parseOutput( 66 | outputAfterLastLineClear 67 | ); 68 | 69 | expect(parsedOutputAfterLastLineClear.stringOutput).toBe( 70 | `? Select project to open:› \ntests \nutils` 71 | ); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /tests/utils/cleanup.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const dotProjectMan = path.join(__dirname, '..', 'dotprojectman'); 5 | const cleanUp = () => { 6 | fs.rmSync(dotProjectMan, { recursive: true }); 7 | }; 8 | 9 | module.exports = cleanUp; 10 | -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type ChoiceType = { 2 | title: string; 3 | }; 4 | 5 | export type ProjectType = { 6 | name: string; 7 | path: string; 8 | editor?: string; 9 | }; 10 | 11 | export type SettingsType = { 12 | commandToOpen: string; 13 | projects: ProjectType[]; 14 | }; 15 | --------------------------------------------------------------------------------