├── .gitignore ├── README.md ├── main.js ├── package-lock.json ├── package.json ├── res ├── github-social.png ├── spaceman.gif └── splash.png └── src ├── tasks.js ├── utils ├── enquirer.js ├── index.js ├── package.js ├── shell.js ├── spaceman.js └── vars.js └── workspaces.js /.gitignore: -------------------------------------------------------------------------------- 1 | .code 2 | .idea 3 | /node_modules 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spaceman 2 | 3 | > Manage monorepo workspaces with a prompt-based CLI 4 | 5 |

6 | Spaceman 7 |

8 | 9 | ## Abstract 10 | 11 | [Monorepos](https://turbo.build/repo/docs/handbook/what-is-a-monorepo) provide a way to manage multiple self-contained applications and packages within a single codebase: 12 | 13 | ``` 14 | +- my-awesome-app 15 | +- apps 16 | | +- backend 17 | | | +- package.json 18 | | +- frontend 19 | | | +- package.json 20 | +- packages 21 | | +- tools 22 | | | +- package.json 23 | | +- utils 24 | | +- package.json 25 | + package.json 26 | ``` 27 | 28 | [Workspaces](https://turbo.build/repo/docs/handbook/workspaces) are the building blocks of monorepos, but require a certain amount of knowledge, configuration and terminal-fu for everyday tasks. 29 | 30 | Spaceman simplifies complex or multistep workspace tasks by presenting them as prompts, and batching commands on confirmation: 31 | 32 |

33 | Spaceman CLI 34 |

35 | 36 | Why read the docs when you can just answer questions? 37 | 38 | Spaceman supports [NPM](https://docs.npmjs.com/cli/v8/using-npm/workspaces) and [Yarn](https://classic.yarnpkg.com/lang/en/docs/workspaces/) with support for [PNPM](https://pnpm.io/workspaces) coming in the next release. It also plays nice with monorepo tools such as [Turborepo](https://turborepo.org/), [Lerna](https://lerna.js.org/) and [Rush](https://rushjs.io/). 39 | 40 | ## Overview 41 | 42 | The following tasks are available: 43 | 44 | **Scripts** 45 | 46 | - [Run](#run)
47 | Run any root or package script 48 | 49 | **Packages** 50 | 51 | - [Install](#install)
52 | Install one or more packages to a workspace 53 | - [Uninstall](#uninstall)
54 | Uninstall one or more packages from a workspace 55 | - [Update](#update)
56 | Update one or more packages in a workspace 57 | - [Reset](#reset)
58 | Remove all Node modules-related files in the root and all workspaces, and reinstall 59 | 60 | **Workspaces** 61 | 62 | - [Share](#share)
63 | Make a workspace available for use within another workspace 64 | - [Group](#group)
65 | Add a new top-level workspace group 66 | - [Add](#add)
67 | Add a new workspace 68 | - [Remove](#remove)
69 | Remove an existing workspace 70 | 71 | ## Setup 72 | 73 | Install the library via NPM: 74 | 75 | ```bash 76 | npm i spaceman --save-dev 77 | ``` 78 | 79 | ## Usage 80 | 81 | Run the library by typing its name: 82 | 83 | ```bash 84 | spaceman 85 | ``` 86 | 87 | You should immediately see set of navigable tasks: 88 | 89 | ``` 90 | ? 🚀 Task … 91 | Scripts 92 | ❯ run 93 | Packages 94 | install 95 | uninstall 96 | update 97 | reset 98 | Workspaces 99 | share 100 | group 101 | add 102 | remove 103 | ``` 104 | 105 | To run a specific task, pass the task name as a second argument: 106 | 107 | ``` 108 | spaceman install 109 | ``` 110 | 111 | Choose a task to run it and view further options: 112 | 113 | ``` 114 | ✔ 🚀 Task · install 115 | ? Workspace … 116 | apps 117 | ❯ docs 118 | web 119 | packages 120 | eslint-config-custom 121 | tsconfig 122 | ui 123 | ``` 124 | 125 | The choices should be self-explanatory, but check the documentation below for more detail. 126 | 127 | # Tasks 128 | 129 | ## Scripts 130 | 131 | ### Run 132 | 133 | Run any root or package script: 134 | 135 | ``` 136 | Script - type to filter scripts (use spaces for partial matching) 137 | ``` 138 | 139 | Confirming will run the selected script. 140 | 141 | See [Configuration](#configuration) for additional options. 142 | 143 | ## Packages 144 | 145 | ### Install 146 | 147 | Install one or more packages to a workspace: 148 | 149 | ``` 150 | Workspace - pick the target workspace to install to 151 | Packages - type a space-separated list of packages to install 152 | Dependency type - pick one of normal, development, peer 153 | ``` 154 | 155 | Confirming will install the new packages. 156 | 157 | ### Uninstall 158 | 159 | Uninstall one or more packages from a workspace: 160 | 161 | ``` 162 | Workspace - pick the target workspace to uninstall from 163 | Packages - pick one or more packages to uninstall 164 | ``` 165 | 166 | Confirming will remove the selected packages. 167 | 168 | ### Update 169 | 170 | Update one or more packages in a workspace: 171 | 172 | ``` 173 | Workspace - pick the target workspace to update 174 | Packages - type a space-separated list of packages to install 175 | ``` 176 | 177 | Confirming will update the selected packages. 178 | 179 | ### Reset 180 | 181 | Remove all Node modules-related files in the root and all workspaces, and reinstall: 182 | 183 | ``` 184 | Confirm reset? - confirm to reset root and workspaces 185 | ``` 186 | 187 | Confirming will: 188 | 189 | - remove all `lock` files 190 | - remove all `node_modules` folders 191 | - re-run `npm|pnpm|yarn install` 192 | 193 | Running `reset` can get you out of tricky situations where workspace installs [fail](https://github.com/npm/cli/issues/3847) or your IDE reports that seemingly-installed workspaces aren't. 194 | 195 | ## Workspaces 196 | 197 | ### Share 198 | 199 | Make a workspace available for use within another workspace: 200 | 201 | ``` 202 | Source workspace - pick the source workspace to share 203 | Target workspace(s) - pick the target workspace(s) to update 204 | ``` 205 | 206 | Confirming will: 207 | 208 | - set the source workspace as a dependency of the target workspace 209 | - run `npm|pnpm|yarn install` 210 | 211 | ### Group 212 | 213 | Add a new workspace group: 214 | 215 | ``` 216 | Group name - type a name for the new group 217 | ``` 218 | 219 | Confirming will: 220 | 221 | - create a new top-level folder 222 | - add it to the list of workspaces in `package.json` 223 | - ask if the user wants to [add](#add) a new workspace 224 | 225 | ### Add 226 | 227 | Add a new workspace: 228 | 229 | ``` 230 | Workspace group - pick the target workspace group 231 | Workspace info 232 | - Workspace - add name, optional description and `main` file 233 | - Dependencies - add optional dependencies 234 | - Scripts - add optional scripts 235 | ``` 236 | 237 | Confirming will: 238 | 239 | - create a new workspace folder 240 | - create a private package file 241 | - create a stub `"main": "index.ts/js"` file with named export 242 | - optionally install dependencies 243 | 244 | ### Remove 245 | 246 | Remove an existing workspace: 247 | 248 | ``` 249 | Workspace - pick the target workspace 250 | Type to confirm - type the name of the workspace to confirm deletion 251 | ``` 252 | 253 | Confirming will: 254 | 255 | - remove the dependency from other workspaces 256 | - uninstall workspace dependencies 257 | - remove the workspace folder 258 | - optionally update the `workspaces` list 259 | 260 | 261 | ## Configuration 262 | 263 | Some of Spaceman's tasks can be configured. 264 | 265 | To do this, add a `spaceman` section to your `package.json` and include the relevant sections: 266 | 267 | ```json5 268 | { 269 | "spaceman": { 270 | "scripts": { 271 | // regexp to exclude scripts from `run` list, e.g. scripts that start with ~ 272 | "exclude": "^~", 273 | 274 | // autocomplete match algorithm; choose between "tight" (default) or "loose" 275 | "match": "loose", 276 | } 277 | } 278 | } 279 | ``` 280 | 281 | Some information on the `script.match` types: 282 | 283 | - `tight`: matches on sequential characters, use spaces to start new match groups, i.e. `cli dev` 284 | - `loose`: matches on any character, i.e. `clde` 285 | 286 | ## Finally... 287 | 288 | If you like the package, a [tweet](https://twitter.com/intent/tweet?text=🧑‍🚀%20Spaceman%20is%20a%20new%20package%20by%20%40dave_stewart%20to%20easily%20manage%20NPM%20and%20Yarn%20monorepo%20tasks%20via%20a%20prompt-based%20CLI%20🚀%0A%0Ahttps%3A//github.com/davestewart/spaceman%0A%0A%23javascript%20%23node%20%23monorepo) is always helpful; be sure to let me know via [@dave_stewart](https://twitter.com/dave_stewart). 289 | 290 | Thanks! 291 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('colors') 3 | yargs = require('yargs/yargs') 4 | const { hideBin } = require('yargs/helpers') 5 | const { runTask, chooseTask } = require('./src/tasks') 6 | 7 | // args 8 | const argv = yargs(hideBin(process.argv)).argv 9 | const [task] = argv._ 10 | 11 | // tasks 12 | console.log() 13 | task 14 | ? runTask(task) 15 | : chooseTask() 16 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spaceman", 3 | "version": "1.4.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "spaceman", 9 | "version": "1.4.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "colors": "^1.4.0", 13 | "enquirer": "^2.3.6", 14 | "rimraf": "^3.0.2", 15 | "shelljs": "^0.8.5", 16 | "yargs": "^17.6.0" 17 | }, 18 | "bin": { 19 | "spaceman": "main.js" 20 | } 21 | }, 22 | "node_modules/ansi-colors": { 23 | "version": "4.1.3", 24 | "license": "MIT", 25 | "engines": { 26 | "node": ">=6" 27 | } 28 | }, 29 | "node_modules/ansi-regex": { 30 | "version": "5.0.1", 31 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 32 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 33 | "engines": { 34 | "node": ">=8" 35 | } 36 | }, 37 | "node_modules/ansi-styles": { 38 | "version": "4.3.0", 39 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 40 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 41 | "dependencies": { 42 | "color-convert": "^2.0.1" 43 | }, 44 | "engines": { 45 | "node": ">=8" 46 | }, 47 | "funding": { 48 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 49 | } 50 | }, 51 | "node_modules/balanced-match": { 52 | "version": "1.0.2", 53 | "license": "MIT" 54 | }, 55 | "node_modules/brace-expansion": { 56 | "version": "1.1.11", 57 | "license": "MIT", 58 | "dependencies": { 59 | "balanced-match": "^1.0.0", 60 | "concat-map": "0.0.1" 61 | } 62 | }, 63 | "node_modules/cliui": { 64 | "version": "8.0.1", 65 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 66 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 67 | "dependencies": { 68 | "string-width": "^4.2.0", 69 | "strip-ansi": "^6.0.1", 70 | "wrap-ansi": "^7.0.0" 71 | }, 72 | "engines": { 73 | "node": ">=12" 74 | } 75 | }, 76 | "node_modules/color-convert": { 77 | "version": "2.0.1", 78 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 79 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 80 | "dependencies": { 81 | "color-name": "~1.1.4" 82 | }, 83 | "engines": { 84 | "node": ">=7.0.0" 85 | } 86 | }, 87 | "node_modules/color-name": { 88 | "version": "1.1.4", 89 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 90 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 91 | }, 92 | "node_modules/colors": { 93 | "version": "1.4.0", 94 | "license": "MIT", 95 | "engines": { 96 | "node": ">=0.1.90" 97 | } 98 | }, 99 | "node_modules/concat-map": { 100 | "version": "0.0.1", 101 | "license": "MIT" 102 | }, 103 | "node_modules/emoji-regex": { 104 | "version": "8.0.0", 105 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 106 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 107 | }, 108 | "node_modules/enquirer": { 109 | "version": "2.3.6", 110 | "license": "MIT", 111 | "dependencies": { 112 | "ansi-colors": "^4.1.1" 113 | }, 114 | "engines": { 115 | "node": ">=8.6" 116 | } 117 | }, 118 | "node_modules/escalade": { 119 | "version": "3.1.1", 120 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 121 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 122 | "engines": { 123 | "node": ">=6" 124 | } 125 | }, 126 | "node_modules/fs.realpath": { 127 | "version": "1.0.0", 128 | "license": "ISC" 129 | }, 130 | "node_modules/function-bind": { 131 | "version": "1.1.1", 132 | "license": "MIT" 133 | }, 134 | "node_modules/get-caller-file": { 135 | "version": "2.0.5", 136 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 137 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 138 | "engines": { 139 | "node": "6.* || 8.* || >= 10.*" 140 | } 141 | }, 142 | "node_modules/glob": { 143 | "version": "7.2.3", 144 | "license": "ISC", 145 | "dependencies": { 146 | "fs.realpath": "^1.0.0", 147 | "inflight": "^1.0.4", 148 | "inherits": "2", 149 | "minimatch": "^3.1.1", 150 | "once": "^1.3.0", 151 | "path-is-absolute": "^1.0.0" 152 | }, 153 | "engines": { 154 | "node": "*" 155 | }, 156 | "funding": { 157 | "url": "https://github.com/sponsors/isaacs" 158 | } 159 | }, 160 | "node_modules/has": { 161 | "version": "1.0.3", 162 | "license": "MIT", 163 | "dependencies": { 164 | "function-bind": "^1.1.1" 165 | }, 166 | "engines": { 167 | "node": ">= 0.4.0" 168 | } 169 | }, 170 | "node_modules/inflight": { 171 | "version": "1.0.6", 172 | "license": "ISC", 173 | "dependencies": { 174 | "once": "^1.3.0", 175 | "wrappy": "1" 176 | } 177 | }, 178 | "node_modules/inherits": { 179 | "version": "2.0.4", 180 | "license": "ISC" 181 | }, 182 | "node_modules/interpret": { 183 | "version": "1.4.0", 184 | "license": "MIT", 185 | "engines": { 186 | "node": ">= 0.10" 187 | } 188 | }, 189 | "node_modules/is-core-module": { 190 | "version": "2.10.0", 191 | "license": "MIT", 192 | "dependencies": { 193 | "has": "^1.0.3" 194 | }, 195 | "funding": { 196 | "url": "https://github.com/sponsors/ljharb" 197 | } 198 | }, 199 | "node_modules/is-fullwidth-code-point": { 200 | "version": "3.0.0", 201 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 202 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 203 | "engines": { 204 | "node": ">=8" 205 | } 206 | }, 207 | "node_modules/minimatch": { 208 | "version": "3.1.2", 209 | "license": "ISC", 210 | "dependencies": { 211 | "brace-expansion": "^1.1.7" 212 | }, 213 | "engines": { 214 | "node": "*" 215 | } 216 | }, 217 | "node_modules/once": { 218 | "version": "1.4.0", 219 | "license": "ISC", 220 | "dependencies": { 221 | "wrappy": "1" 222 | } 223 | }, 224 | "node_modules/path-is-absolute": { 225 | "version": "1.0.1", 226 | "license": "MIT", 227 | "engines": { 228 | "node": ">=0.10.0" 229 | } 230 | }, 231 | "node_modules/path-parse": { 232 | "version": "1.0.7", 233 | "license": "MIT" 234 | }, 235 | "node_modules/rechoir": { 236 | "version": "0.6.2", 237 | "dependencies": { 238 | "resolve": "^1.1.6" 239 | }, 240 | "engines": { 241 | "node": ">= 0.10" 242 | } 243 | }, 244 | "node_modules/require-directory": { 245 | "version": "2.1.1", 246 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 247 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 248 | "engines": { 249 | "node": ">=0.10.0" 250 | } 251 | }, 252 | "node_modules/resolve": { 253 | "version": "1.22.1", 254 | "license": "MIT", 255 | "dependencies": { 256 | "is-core-module": "^2.9.0", 257 | "path-parse": "^1.0.7", 258 | "supports-preserve-symlinks-flag": "^1.0.0" 259 | }, 260 | "bin": { 261 | "resolve": "bin/resolve" 262 | }, 263 | "funding": { 264 | "url": "https://github.com/sponsors/ljharb" 265 | } 266 | }, 267 | "node_modules/rimraf": { 268 | "version": "3.0.2", 269 | "license": "ISC", 270 | "dependencies": { 271 | "glob": "^7.1.3" 272 | }, 273 | "bin": { 274 | "rimraf": "bin.js" 275 | }, 276 | "funding": { 277 | "url": "https://github.com/sponsors/isaacs" 278 | } 279 | }, 280 | "node_modules/shelljs": { 281 | "version": "0.8.5", 282 | "license": "BSD-3-Clause", 283 | "dependencies": { 284 | "glob": "^7.0.0", 285 | "interpret": "^1.0.0", 286 | "rechoir": "^0.6.2" 287 | }, 288 | "bin": { 289 | "shjs": "bin/shjs" 290 | }, 291 | "engines": { 292 | "node": ">=4" 293 | } 294 | }, 295 | "node_modules/string-width": { 296 | "version": "4.2.3", 297 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 298 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 299 | "dependencies": { 300 | "emoji-regex": "^8.0.0", 301 | "is-fullwidth-code-point": "^3.0.0", 302 | "strip-ansi": "^6.0.1" 303 | }, 304 | "engines": { 305 | "node": ">=8" 306 | } 307 | }, 308 | "node_modules/strip-ansi": { 309 | "version": "6.0.1", 310 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 311 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 312 | "dependencies": { 313 | "ansi-regex": "^5.0.1" 314 | }, 315 | "engines": { 316 | "node": ">=8" 317 | } 318 | }, 319 | "node_modules/supports-preserve-symlinks-flag": { 320 | "version": "1.0.0", 321 | "license": "MIT", 322 | "engines": { 323 | "node": ">= 0.4" 324 | }, 325 | "funding": { 326 | "url": "https://github.com/sponsors/ljharb" 327 | } 328 | }, 329 | "node_modules/wrap-ansi": { 330 | "version": "7.0.0", 331 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 332 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 333 | "dependencies": { 334 | "ansi-styles": "^4.0.0", 335 | "string-width": "^4.1.0", 336 | "strip-ansi": "^6.0.0" 337 | }, 338 | "engines": { 339 | "node": ">=10" 340 | }, 341 | "funding": { 342 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 343 | } 344 | }, 345 | "node_modules/wrappy": { 346 | "version": "1.0.2", 347 | "license": "ISC" 348 | }, 349 | "node_modules/y18n": { 350 | "version": "5.0.8", 351 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 352 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 353 | "engines": { 354 | "node": ">=10" 355 | } 356 | }, 357 | "node_modules/yargs": { 358 | "version": "17.6.0", 359 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", 360 | "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", 361 | "dependencies": { 362 | "cliui": "^8.0.1", 363 | "escalade": "^3.1.1", 364 | "get-caller-file": "^2.0.5", 365 | "require-directory": "^2.1.1", 366 | "string-width": "^4.2.3", 367 | "y18n": "^5.0.5", 368 | "yargs-parser": "^21.0.0" 369 | }, 370 | "engines": { 371 | "node": ">=12" 372 | } 373 | }, 374 | "node_modules/yargs-parser": { 375 | "version": "21.1.1", 376 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 377 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 378 | "engines": { 379 | "node": ">=12" 380 | } 381 | } 382 | }, 383 | "dependencies": { 384 | "ansi-colors": { 385 | "version": "4.1.3" 386 | }, 387 | "ansi-regex": { 388 | "version": "5.0.1", 389 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 390 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 391 | }, 392 | "ansi-styles": { 393 | "version": "4.3.0", 394 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 395 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 396 | "requires": { 397 | "color-convert": "^2.0.1" 398 | } 399 | }, 400 | "balanced-match": { 401 | "version": "1.0.2" 402 | }, 403 | "brace-expansion": { 404 | "version": "1.1.11", 405 | "requires": { 406 | "balanced-match": "^1.0.0", 407 | "concat-map": "0.0.1" 408 | } 409 | }, 410 | "cliui": { 411 | "version": "8.0.1", 412 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 413 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 414 | "requires": { 415 | "string-width": "^4.2.0", 416 | "strip-ansi": "^6.0.1", 417 | "wrap-ansi": "^7.0.0" 418 | } 419 | }, 420 | "color-convert": { 421 | "version": "2.0.1", 422 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 423 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 424 | "requires": { 425 | "color-name": "~1.1.4" 426 | } 427 | }, 428 | "color-name": { 429 | "version": "1.1.4", 430 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 431 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 432 | }, 433 | "colors": { 434 | "version": "1.4.0" 435 | }, 436 | "concat-map": { 437 | "version": "0.0.1" 438 | }, 439 | "emoji-regex": { 440 | "version": "8.0.0", 441 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 442 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 443 | }, 444 | "enquirer": { 445 | "version": "2.3.6", 446 | "requires": { 447 | "ansi-colors": "^4.1.1" 448 | } 449 | }, 450 | "escalade": { 451 | "version": "3.1.1", 452 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 453 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 454 | }, 455 | "fs.realpath": { 456 | "version": "1.0.0" 457 | }, 458 | "function-bind": { 459 | "version": "1.1.1" 460 | }, 461 | "get-caller-file": { 462 | "version": "2.0.5", 463 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 464 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 465 | }, 466 | "glob": { 467 | "version": "7.2.3", 468 | "requires": { 469 | "fs.realpath": "^1.0.0", 470 | "inflight": "^1.0.4", 471 | "inherits": "2", 472 | "minimatch": "^3.1.1", 473 | "once": "^1.3.0", 474 | "path-is-absolute": "^1.0.0" 475 | } 476 | }, 477 | "has": { 478 | "version": "1.0.3", 479 | "requires": { 480 | "function-bind": "^1.1.1" 481 | } 482 | }, 483 | "inflight": { 484 | "version": "1.0.6", 485 | "requires": { 486 | "once": "^1.3.0", 487 | "wrappy": "1" 488 | } 489 | }, 490 | "inherits": { 491 | "version": "2.0.4" 492 | }, 493 | "interpret": { 494 | "version": "1.4.0" 495 | }, 496 | "is-core-module": { 497 | "version": "2.10.0", 498 | "requires": { 499 | "has": "^1.0.3" 500 | } 501 | }, 502 | "is-fullwidth-code-point": { 503 | "version": "3.0.0", 504 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 505 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 506 | }, 507 | "minimatch": { 508 | "version": "3.1.2", 509 | "requires": { 510 | "brace-expansion": "^1.1.7" 511 | } 512 | }, 513 | "once": { 514 | "version": "1.4.0", 515 | "requires": { 516 | "wrappy": "1" 517 | } 518 | }, 519 | "path-is-absolute": { 520 | "version": "1.0.1" 521 | }, 522 | "path-parse": { 523 | "version": "1.0.7" 524 | }, 525 | "rechoir": { 526 | "version": "0.6.2", 527 | "requires": { 528 | "resolve": "^1.1.6" 529 | } 530 | }, 531 | "require-directory": { 532 | "version": "2.1.1", 533 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 534 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" 535 | }, 536 | "resolve": { 537 | "version": "1.22.1", 538 | "requires": { 539 | "is-core-module": "^2.9.0", 540 | "path-parse": "^1.0.7", 541 | "supports-preserve-symlinks-flag": "^1.0.0" 542 | } 543 | }, 544 | "rimraf": { 545 | "version": "3.0.2", 546 | "requires": { 547 | "glob": "^7.1.3" 548 | } 549 | }, 550 | "shelljs": { 551 | "version": "0.8.5", 552 | "requires": { 553 | "glob": "^7.0.0", 554 | "interpret": "^1.0.0", 555 | "rechoir": "^0.6.2" 556 | } 557 | }, 558 | "string-width": { 559 | "version": "4.2.3", 560 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 561 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 562 | "requires": { 563 | "emoji-regex": "^8.0.0", 564 | "is-fullwidth-code-point": "^3.0.0", 565 | "strip-ansi": "^6.0.1" 566 | } 567 | }, 568 | "strip-ansi": { 569 | "version": "6.0.1", 570 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 571 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 572 | "requires": { 573 | "ansi-regex": "^5.0.1" 574 | } 575 | }, 576 | "supports-preserve-symlinks-flag": { 577 | "version": "1.0.0" 578 | }, 579 | "wrap-ansi": { 580 | "version": "7.0.0", 581 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 582 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 583 | "requires": { 584 | "ansi-styles": "^4.0.0", 585 | "string-width": "^4.1.0", 586 | "strip-ansi": "^6.0.0" 587 | } 588 | }, 589 | "wrappy": { 590 | "version": "1.0.2" 591 | }, 592 | "y18n": { 593 | "version": "5.0.8", 594 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 595 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 596 | }, 597 | "yargs": { 598 | "version": "17.6.0", 599 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", 600 | "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", 601 | "requires": { 602 | "cliui": "^8.0.1", 603 | "escalade": "^3.1.1", 604 | "get-caller-file": "^2.0.5", 605 | "require-directory": "^2.1.1", 606 | "string-width": "^4.2.3", 607 | "y18n": "^5.0.5", 608 | "yargs-parser": "^21.0.0" 609 | } 610 | }, 611 | "yargs-parser": { 612 | "version": "21.1.1", 613 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 614 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" 615 | } 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spaceman", 3 | "version": "1.5.3", 4 | "description": "Manage monorepo workspaces with a prompt-based CLI", 5 | "author": "Dave Stewart", 6 | "license": "ISC", 7 | "keywords": [ 8 | "monorepo", 9 | "turborepo", 10 | "vercel", 11 | "yarn", 12 | "pnpm", 13 | "npm" 14 | ], 15 | "main": "main.js", 16 | "files": [ 17 | "main.js", 18 | "src" 19 | ], 20 | "bin": { 21 | "spaceman": "main.js" 22 | }, 23 | "dependencies": { 24 | "colors": "^1.4.0", 25 | "enquirer": "^2.3.6", 26 | "rimraf": "^3.0.2", 27 | "shelljs": "^0.8.5", 28 | "yargs": "^17.6.0" 29 | }, 30 | "homepage": "https://github.com/davestewart/spaceman#readme", 31 | "bugs": "https://github.com/davestewart/spaceman/issues", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/davestewart/spaceman.git" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /res/github-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davestewart/spaceman/7e1b74bea898628bd7914cde6b13fcd8f56b217e/res/github-social.png -------------------------------------------------------------------------------- /res/spaceman.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davestewart/spaceman/7e1b74bea898628bd7914cde6b13fcd8f56b217e/res/spaceman.gif -------------------------------------------------------------------------------- /res/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davestewart/spaceman/7e1b74bea898628bd7914cde6b13fcd8f56b217e/res/splash.png -------------------------------------------------------------------------------- /src/tasks.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs') 2 | const rimraf = require('rimraf') 3 | const { toSentence, toCamel, uniq, sortObject, removeItem, toArray } = require('./utils') 4 | const { ask, confirm, _ask, _heading, makeChoicesGroup } = require('./utils/enquirer') 5 | const { log, exec, exit } = require('./utils/shell') 6 | const { getSetting } = require('./utils/spaceman') 7 | const { ROOT } = require('./utils/vars') 8 | const { 9 | getWorkspaces, 10 | getWorkspacesChoices, 11 | getWorkspace, 12 | getWorkspacePath, 13 | getWorkspaceGroups, 14 | getWorkspaceGroupFolders, 15 | } = require('./workspaces') 16 | const { 17 | isValidName, 18 | getCommand, 19 | getScripts, 20 | getDependencies, 21 | readPackage, 22 | writePackage, 23 | getManager, 24 | } = require('./utils/package') 25 | 26 | // --------------------------------------------------------------------------------------------------------------------- 27 | // region Packages 28 | // --------------------------------------------------------------------------------------------------------------------- 29 | 30 | function chooseScript (input = {}) { 31 | // helper 32 | const makeChoice = (path, name) => { 33 | const value = `${path} ${name}` 34 | const message = path 35 | ? path.replace(/^\//, '').grey + ' ' + name 36 | : name 37 | return { 38 | value, 39 | message, 40 | indent: ' ', 41 | data: { 42 | path, 43 | name, 44 | } 45 | } 46 | } 47 | 48 | // make options 49 | const main = getScripts().map(name => makeChoice('', name)) 50 | const other = getWorkspaces() 51 | .reduce((items, workspace) => { 52 | const scripts = getScripts(workspace.path).map(name => makeChoice(workspace.path, name)) 53 | items.push(...scripts) 54 | return items 55 | }, []) 56 | 57 | 58 | // settings 59 | const { exclude, match } = getSetting('scripts', {}) 60 | 61 | // set up exclusion filter 62 | const rxExclude = exclude ? new RegExp(String(exclude)) : null 63 | const fnInclude = rxExclude 64 | ? choice => !rxExclude.test(choice.data.name) 65 | : () => true 66 | 67 | // set up match algorithm 68 | const suggest = (text, choices) => { 69 | const rx = match === 'loose' 70 | ? new RegExp(text.split(/\s+/).map(text => text.split('').join('.*')).join('.*\\b.+')) 71 | : new RegExp(text.replace(/\s+/g, '.*')) 72 | return choices.filter(choice => rx.test(choice.value)) 73 | } 74 | 75 | // build items 76 | const choices = [...main, ...other].filter(fnInclude) 77 | 78 | // options 79 | const options = { 80 | type: 'autocomplete', 81 | choices, 82 | limit: 10, 83 | suggest, 84 | result (value) { 85 | return choices.find(item => item.value === value).data 86 | }, 87 | } 88 | return ask('script', 'Script', options, input) 89 | } 90 | 91 | /** 92 | * Choose a package to install 93 | * 94 | * @param {{ task: string, workspace: string }} input Input from previous prompt 95 | * @returns {Promise<{ task: string, workspace: string, packages: string }>} 96 | */ 97 | function choosePackages (input = {}) { 98 | // variables 99 | const name = 'packages' 100 | const message = 'Package(s)' 101 | 102 | // install 103 | if (input.task === 'install') { 104 | const options = { 105 | validate: answer => { 106 | answer = answer.trim() 107 | if (answer === '') { 108 | return 'Type one or more packages separated by spaces' 109 | } 110 | return !!answer 111 | }, 112 | } 113 | return ask(name, message, options, input).then(chooseDepType) 114 | } 115 | 116 | // uninstall / update 117 | else { 118 | const workspace = getWorkspace(input.workspace) 119 | const choices = getDependencies(workspace.path) 120 | if (choices.length === 0) { 121 | console.log(`\nWorkspace does not contain any packages to ${input.task}`) 122 | exit() 123 | } 124 | const options = { 125 | type: 'multiselect', 126 | choices, 127 | result (answer) { 128 | return answer.join(' ') 129 | }, 130 | } 131 | return ask(name, message, options, input) 132 | } 133 | } 134 | 135 | /** 136 | * Choose a dependency type 137 | * 138 | * @param {object} input Input from previous prompt 139 | * @returns {Promise<{ depType: string }>} 140 | */ 141 | function chooseDepType (input = {}) { 142 | const depTypes = { 143 | normal: '', 144 | development: 'dev', 145 | peer: 'peer', 146 | } 147 | const options = { 148 | choices: [ 149 | 'normal', 150 | 'development', 151 | 'peer', 152 | ], 153 | result (answer) { 154 | return depTypes[answer] 155 | }, 156 | } 157 | return ask('depType', 'Dependency type', options, input) 158 | } 159 | 160 | // endregion 161 | // --------------------------------------------------------------------------------------------------------------------- 162 | // region Workspaces 163 | // --------------------------------------------------------------------------------------------------------------------- 164 | 165 | /** 166 | * Choose a workspace 167 | * 168 | * @param {object} input Input from the previous prompt 169 | * @returns {Promise<{ workspace: string }>} 170 | */ 171 | function chooseWorkspace (input = {}) { 172 | const options = { 173 | choices: getWorkspacesChoices(), 174 | } 175 | return ask('workspace', 'Workspace', options, input) 176 | } 177 | 178 | /** 179 | * Factory function to choose a workspace group, optionally omitting source group 180 | * 181 | * @param {string} type The name of the field to create 182 | * @param {boolean} multi An optional flag to choose multiple workspaces 183 | * @returns {function(*=): *} 184 | */ 185 | function chooseWorkspaceByType (type = 'source', multi = false) { 186 | /** 187 | * Choose a workspace 188 | * 189 | * @param {{ [type]: string }} input Input from previous prompt 190 | * @returns {Promise<{ [type]: string }>} 191 | */ 192 | return function (input = {}) { 193 | const options = { 194 | type: multi ? 'multiselect' : 'select', 195 | choices: getWorkspacesChoices(getWorkspaces().filter(workspace => workspace.name !== input.source)), 196 | } 197 | const message = `${toSentence(type)} workspace` 198 | return ask(type, multi ? `${message}(s)` : message, options, input) 199 | } 200 | } 201 | 202 | /** 203 | * Choose a workspace group 204 | * 205 | * @param {object} input Input from previous prompt 206 | * @returns {Promise<{ group: string }>} 207 | */ 208 | function chooseWorkspaceGroupName (input = {}) { 209 | const options = { 210 | validate (answer) { 211 | const name = answer.trim() 212 | if (!name) { 213 | return 'The workspace group must be named' 214 | } 215 | if (!isValidName(name)) { 216 | return 'Workspace group must be a valid folder name' 217 | } 218 | if (Fs.existsSync(`./${name}`)) { 219 | return 'Workspace group conflicts with existing folder' 220 | } 221 | return !!name 222 | }, 223 | result (answer) { 224 | return answer.trim().toLowerCase() + '/*' 225 | }, 226 | } 227 | 228 | return ask('group', 'Group name', options, input) 229 | } 230 | 231 | /** 232 | * Choose a workspace group 233 | * 234 | * @param {{ [group]: string }} input Input from previous prompt 235 | * @returns {Promise<{ group: string }>} 236 | */ 237 | function chooseWorkspaceGroup (input = {}) { 238 | if (input.group) { 239 | return Promise.resolve(input) 240 | } 241 | const options = { 242 | type: 'select', 243 | choices: [...getWorkspaceGroups(), ROOT], 244 | } 245 | return ask('group', 'Workspace group', options, input) 246 | } 247 | 248 | /** 249 | * Choose workspace options 250 | * 251 | * @param {object} input Input from previous prompt 252 | * @returns {Promise} 253 | */ 254 | function chooseWorkspaceOptions (input = {}) { 255 | return Promise.resolve(input) 256 | // workspace 257 | .then(_heading('Workspace')) 258 | .then(_ask('name', 'Name', { 259 | validate: (name) => { 260 | name = name.trim() 261 | const path = getWorkspacePath(input.group, name) 262 | if (!name) { 263 | return 'The workspace must be named' 264 | } 265 | if (!isValidName(name)) { 266 | return 'Workspace name must be a valid package name' 267 | } 268 | if (getWorkspaces().map(workspace => workspace.name).includes(name)) { 269 | return 'Workspace name must be unique within monorepo' 270 | } 271 | if (Fs.existsSync(`.${path}`)) { 272 | return 'Workspace group cannot be an existing folder' 273 | } 274 | return true 275 | }, 276 | })) 277 | .then(_ask('description', 'Description')) 278 | .then(input => { 279 | const isTypescript = getWorkspaces().some(workspace => Fs.existsSync(`.${workspace.path}/tsconfig.json`)) 280 | const initial = `index.${isTypescript ? 'ts' : 'js'}` 281 | return ask('main', 'Main file', { initial }, input) 282 | }) 283 | 284 | // deps 285 | .then(_heading('Dependencies')) 286 | .then(_ask('deps', 'Main')) 287 | .then(_ask('devs', 'Dev')) 288 | 289 | // scripts 290 | .then(_heading('Scripts')) 291 | .then(_ask('dev', 'Dev')) 292 | .then(_ask('build', 'Build')) 293 | .then(_ask('test', 'Test')) 294 | 295 | // final 296 | .then((options) => { 297 | return { 298 | ...input, 299 | options, 300 | } 301 | }) 302 | } 303 | 304 | /** 305 | * Confirm adding a workspace 306 | * 307 | * @param {object} input Input from previous prompt 308 | * @returns {Promise} 309 | */ 310 | function confirmAddWorkspace (input = {}) { 311 | return confirm('Add new workspace?', input) 312 | .then(() => { 313 | return runTask('add', { group: input.group.replace('/*', '') }) 314 | }) 315 | } 316 | 317 | /** 318 | * Confirm removing a workspace 319 | * 320 | * @param {object} input Input from previous prompt 321 | * @returns {Promise} 322 | */ 323 | function confirmRemoveWorkspace (input = {}) { 324 | const { workspace } = input 325 | const options = { 326 | validate (answer) { 327 | if (answer !== workspace) { 328 | return `Type "${workspace}" to confirm removal` 329 | } 330 | return true 331 | }, 332 | } 333 | return ask('confirm', 'Type workspace folder name to confirm removal'.red, options, input) 334 | } 335 | 336 | // endregion 337 | // --------------------------------------------------------------------------------------------------------------------- 338 | // region Actions 339 | // --------------------------------------------------------------------------------------------------------------------- 340 | 341 | /** 342 | * Runs a script in a specific package 343 | * 344 | * @param {object} input Input from previous prompt 345 | */ 346 | function runScript (input = {}) { 347 | const { path, name } = input.script 348 | const manager = getManager() 349 | const command = manager === 'yarn' 350 | ? 'yarn' 351 | : `${manager} run` 352 | console.log('Running script') 353 | exec(`cd .${path} && ${command} ${name} && exit`) 354 | } 355 | 356 | /** 357 | * Runs an install, uninstall or update command 358 | * 359 | * @param {object} input Input from previous prompt 360 | */ 361 | function runCommand (input = {}) { 362 | const { task, depType, packages, workspace } = input 363 | if (task && packages.length && workspace) { 364 | console.log(`\nRunning: ${task}`) 365 | exec(getCommand(task, depType, workspace, packages)) 366 | } 367 | console.log() 368 | } 369 | 370 | /** 371 | * Removes invalid node_modules related files and folders from the monorepo 372 | * 373 | * @param {object} input Input from previous prompt 374 | */ 375 | function resetPackages (input = {}) { 376 | // helpers 377 | function getPaths (path = '') { 378 | const paths = [ 379 | `.${path}/.turbo`, 380 | `.${path}/yarn.lock`, 381 | `.${path}/package-lock.json`, 382 | `.${path}/node_modules/`, 383 | ] 384 | return paths.filter(path => Fs.existsSync(path)) 385 | } 386 | 387 | function removePaths (paths) { 388 | try { 389 | paths.forEach(path => { 390 | log(`rimraf ${path}`) 391 | rimraf.sync(path) 392 | }) 393 | } 394 | // rimraf can sometimes fail, so try again if it does 395 | catch (err) { 396 | paths.forEach(path => { 397 | rimraf.sync(path) 398 | }) 399 | } 400 | } 401 | 402 | // workspaces 403 | let paths = getWorkspaces().reduce((paths, workspace) => { 404 | paths = [...paths, ...getPaths(workspace.path)] 405 | return paths 406 | }, []) 407 | if (paths.length) { 408 | console.log('\nResetting workspaces:') 409 | removePaths(paths) 410 | } 411 | 412 | // root 413 | paths = getPaths() 414 | if (paths.length) { 415 | console.log('\nResetting root:') 416 | removePaths(paths) 417 | } 418 | 419 | // packages 420 | console.log('\nReinstalling packages:') 421 | exec(getCommand()) 422 | } 423 | 424 | /** 425 | * Creates a workspace group folder and updates package.json 426 | * 427 | * @param {object} input Input from previous prompt 428 | * @returns {object} Returns input in case createWorkspace() is run after 429 | */ 430 | function createWorkspaceGroup (input = {}) { 431 | // variables 432 | const { group } = input 433 | 434 | // folder 435 | const path = group.replace(/\/\*$/, '') 436 | Fs.mkdirSync(`./${path}`) 437 | 438 | // package file 439 | const data = readPackage() 440 | if (!data.workspaces) { 441 | data.workspaces = [] 442 | } 443 | data.workspaces = uniq([...data.workspaces, group]) 444 | writePackage('', data) 445 | 446 | // return input for optional createWorkspace() 447 | return input 448 | } 449 | 450 | /** 451 | * Creates a workspace folder, package file, and optionally installs dependencies 452 | * 453 | * @param {object} input Input from previous prompt 454 | */ 455 | function createWorkspace (input = {}) { 456 | // variables 457 | const { group, options } = input 458 | const { name, description, main, dev, build, test, deps, devs } = options 459 | const folder = name.split('/').pop() // in case user has used a namespace 460 | const path = getWorkspacePath(group, folder) 461 | const data = { 462 | name, 463 | description, 464 | version: '0.0.0', 465 | private: true, 466 | main, 467 | scripts: { 468 | dev: dev || undefined, 469 | build: build || undefined, 470 | test: test || 'echo "Error: no test specified" && exit 1', 471 | }, 472 | } 473 | 474 | // add folder / folder 475 | console.log(`\nCreating folder: ${path.substring(1)}`) 476 | group === ROOT 477 | ? createWorkspaceGroup({ group: folder }) // special case if workspace is in root; update package.json 478 | : Fs.mkdirSync(`.${path}`, { recursive: true }) 479 | 480 | // write package 481 | console.log(`\nWriting: ${path.substring(1)}/package.json`) 482 | writePackage(path, data) 483 | 484 | // write main file 485 | if (main) { 486 | const script = `export function ${toCamel(folder)} () {\n console.log('${name}')\n}\n` 487 | Fs.writeFileSync(`.${path}/${main}`, script, 'utf-8') 488 | } 489 | 490 | // install dependencies 491 | if (deps || devs) { 492 | // HACK: NPM doesn't always pick up on the new package; not sure why? 493 | readPackage(path) 494 | 495 | // install 496 | console.log() 497 | if (deps) { 498 | console.log('Installing dependencies:') 499 | exec(getCommand('install', '', name, deps)) 500 | } 501 | if (devs) { 502 | console.log('Installing dev dependencies:') 503 | exec(getCommand('install', 'dev', name, devs)) 504 | } 505 | } 506 | } 507 | 508 | /** 509 | * Removes a workspace by uninstalling dependencies then removing the folder 510 | * 511 | * @param {object} input Input from previous prompt 512 | */ 513 | function removeWorkspace (input = {}) { 514 | const workspace = getWorkspace(input.workspace) 515 | if (workspace) { 516 | const { name, group, folder, path } = workspace 517 | const pkg = readPackage(path) 518 | if (pkg) { 519 | // get target workspaces 520 | const targets = getWorkspaces() 521 | .map(workspace => { 522 | const dependencies = getDependencies(workspace.path) 523 | if (dependencies.includes(name)) { 524 | return workspace.name 525 | } 526 | }) 527 | .filter(name => name) 528 | 529 | // uninstall from target workspaces 530 | if (targets.length) { 531 | console.log('\nUninstalling from workspaces:') 532 | targets.forEach(target => { 533 | const command = getCommand('uninstall', '', target, name) 534 | exec(command, true) 535 | }) 536 | } 537 | 538 | // get local dependencies 539 | const names = getDependencies(pkg).join(' ') 540 | 541 | // uninstall packages 542 | if (names.length) { 543 | console.log('\nUninstalling dependencies:') 544 | const command = getCommand('uninstall', '', name, names) 545 | exec(command, true) 546 | } 547 | 548 | // remove package folder 549 | // await wait(500) 550 | console.log(`\nRemoving workspace: ${path.substring(1)}`) 551 | exec(`rimraf .${path}`) 552 | 553 | // update workspace config 554 | const pkgMain = readPackage('') 555 | if (pkgMain) { 556 | // remove absolute workspace name 557 | removeItem(pkgMain.workspaces, folder) 558 | 559 | // remove wildcards 560 | const folders = getWorkspaceGroupFolders(group) 561 | if (folders.length === 0) { 562 | console.log('\nRemoving empty workspace group:') 563 | removeItem(pkgMain.workspaces, `${group}/*`) 564 | exec(`rimraf ./${group}`) 565 | } 566 | 567 | // update package 568 | console.log('\nUpdating: package.json') 569 | writePackage('', pkgMain) 570 | } 571 | } 572 | } 573 | } 574 | 575 | /** 576 | * Shares a source workspace with one or more target workspaces by updating their package.json 577 | * 578 | * @param {object} input Input from previous prompt 579 | */ 580 | function shareWorkspace (input = {}) { 581 | // variables 582 | const { source, target } = input 583 | const targets = toArray(target) 584 | 585 | // update targets 586 | console.log() 587 | targets.forEach(target => { 588 | const { path } = getWorkspace(target) 589 | const data = readPackage(path) 590 | if (!data.dependencies) { 591 | data.dependencies = {} 592 | } 593 | data.dependencies[source] = '*' 594 | data.dependencies = sortObject(data.dependencies) 595 | 596 | // update 597 | console.log(`Updating: ${path.substring(1)}/package.json`) 598 | writePackage(path, data) 599 | }) 600 | 601 | // install 602 | console.log('\nInstalling dependencies:') 603 | exec(getCommand(), true) 604 | } 605 | 606 | // endregion 607 | // --------------------------------------------------------------------------------------------------------------------- 608 | // region Tasks 609 | // --------------------------------------------------------------------------------------------------------------------- 610 | 611 | /** 612 | * Task chooser 613 | * 614 | * Builds an Enquirer Select prompt then runs the chosen task 615 | */ 616 | function chooseTask () { 617 | const choices = [ 618 | makeChoicesGroup('Scripts', [ 619 | 'run', 620 | ]), 621 | makeChoicesGroup('Packages', [ 622 | 'install', 623 | 'uninstall', 624 | 'update', 625 | 'reset', 626 | ]), 627 | makeChoicesGroup('Workspaces', [ 628 | 'share', 629 | 'group', 630 | 'add', 631 | 'remove', 632 | ]), 633 | ] 634 | return Promise 635 | .resolve(ask('task', '🚀 Task', { choices })) 636 | .then(input => runTask(input.task)) 637 | } 638 | 639 | /** 640 | * Task runner 641 | * 642 | * How it works: 643 | * 644 | * - prompts are chained with input from the previous prompt 645 | * - prompts should append to and return input after being called 646 | * - this way, after the final prompt, the action will have all user input in a single object 647 | * - failure to do this will either result in an error or unexpected results! 648 | * 649 | * @param {string} task The task to run 650 | * @param {object} [input] Optional input from previous task 651 | * @returns {Promise<{}>|Promise} 652 | */ 653 | function runTask (task, input = {}) { 654 | // prepare input 655 | input = { task, ...input } 656 | 657 | // run task! 658 | switch (task) { 659 | case 'run': 660 | return Promise.resolve(input) 661 | .then(chooseScript) 662 | .then(runScript) 663 | 664 | case 'install': 665 | case 'uninstall': 666 | case 'update': 667 | return Promise.resolve(input) 668 | .then(chooseWorkspace) 669 | .then(choosePackages) 670 | .then(confirmTask) 671 | .then(runCommand) 672 | 673 | case 'reset': 674 | return Promise.resolve(input) 675 | .then(confirmTask) 676 | .then(resetPackages) 677 | 678 | case 'share': 679 | return Promise.resolve(input) 680 | .then(chooseWorkspaceByType('source')) 681 | .then(chooseWorkspaceByType('target', true)) 682 | .then(confirmTask) 683 | .then(shareWorkspace) 684 | .then(exit) 685 | 686 | case 'group': 687 | return Promise.resolve(input) 688 | .then(chooseWorkspaceGroupName) 689 | .then(confirmTask) 690 | .then(createWorkspaceGroup) 691 | .then(confirmAddWorkspace) 692 | 693 | case 'add': 694 | return Promise.resolve(input) 695 | .then(chooseWorkspaceGroup) 696 | .then(chooseWorkspaceOptions) 697 | .then(confirmTask) 698 | .then(createWorkspace) 699 | 700 | case 'remove': 701 | return Promise.resolve(input) 702 | .then(chooseWorkspace) 703 | .then(confirmRemoveWorkspace) 704 | .then(removeWorkspace) 705 | 706 | default: 707 | console.log(`Unknown task "${task}"`) 708 | exit() 709 | } 710 | } 711 | 712 | /** 713 | * Confirm a task 714 | * 715 | * @param {{ task: string }} input Input from previous prompt 716 | * @returns {Promise<{ task: string }>} 717 | */ 718 | function confirmTask (input) { 719 | return confirm(`Confirm ${input.task}?`, input) 720 | } 721 | 722 | // --------------------------------------------------------------------------------------------------------------------- 723 | // export chooser 724 | // --------------------------------------------------------------------------------------------------------------------- 725 | 726 | module.exports = { 727 | runTask, 728 | chooseTask, 729 | } 730 | -------------------------------------------------------------------------------- /src/utils/enquirer.js: -------------------------------------------------------------------------------- 1 | const { prompt } = require('enquirer') 2 | const { exit } = require('./shell') 3 | 4 | /** 5 | * @typedef {object} PromptOptions 6 | * @property {Function} [validate] 7 | * @property {Function} [result] 8 | * @property {string} [type] 9 | * @property {*[]} [choices] 10 | * @property {*} [initial] 11 | */ 12 | 13 | function makeChoicesGroup (heading, choices) { 14 | return { 15 | role: 'heading', 16 | value: heading.red, 17 | choices, 18 | } 19 | } 20 | 21 | /** 22 | * Chainable 23 | * 24 | * @param {string} name The name of the field 25 | * @param {string} message The label to show in the prompt 26 | * @param {PromptOptions} [options] Optional options 27 | * @param {object} [input] Optional input 28 | * @returns {Promise} The updated input 29 | */ 30 | function ask (name, message, options = {}, input = {}) { 31 | // defaults 32 | const type = 'input' 33 | const validate = () => true 34 | 35 | // select & multiselect 36 | if (options.choices && !options.type) { 37 | options.type = 'select' 38 | } 39 | if (options.type === 'multiselect' && !options.validate) { 40 | options.validate = function (answer) { 41 | if (answer.length === 0) { 42 | return 'You must choose at least one item' 43 | } 44 | return true 45 | } 46 | } 47 | 48 | // prompt 49 | return prompt({ type, name, message, validate, ...options }) 50 | .then(response => { 51 | const answer = response[name] 52 | if (typeof answer === 'string') { 53 | response[name] = answer.trim() 54 | } 55 | return { ...input, ...response } 56 | }) 57 | .catch(exit) 58 | } 59 | 60 | function heading (text, input = {}) { 61 | console.log(`\n ${text} :`.grey) 62 | return input 63 | } 64 | 65 | function confirm (message, input = {}) { 66 | const options = { 67 | type: 'confirm', 68 | name: 'confirm', 69 | message, 70 | initial: true, 71 | } 72 | return prompt(options) 73 | .then(answer => { 74 | return answer['confirm'] 75 | ? input 76 | : exit() 77 | }) 78 | .catch(exit) 79 | } 80 | 81 | // wrapped versions of prompts which can be passed to promises 82 | const _ask = (name, message, options) => (input) => ask(name, message, options, input) 83 | const _heading = (message) => (input) => heading(message, input) 84 | const _confirm = (message) => (input) => confirm(message, input) 85 | 86 | module.exports = { 87 | makeChoicesGroup, 88 | ask, 89 | heading, 90 | confirm, 91 | _ask, 92 | _heading, 93 | _confirm, 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | function toSentence (value) { 2 | return value.replace(/\w/, c => c.toUpperCase()) 3 | } 4 | 5 | function toCamel (value) { 6 | return value 7 | .replace(/^\W+|\W$/g, '') 8 | .replace(/(\W+)(\w)/g, (all, a, b) => b.toUpperCase()) 9 | } 10 | 11 | function sortObject (data) { 12 | const keys = Object.keys(data).sort() 13 | return keys.reduce((output, key) => { 14 | output[key] = data[key] 15 | return output 16 | }, {}) 17 | } 18 | 19 | function getProperty (data, path = '', defaults = undefined) { 20 | if (data && typeof path === 'string') { 21 | const keys = path.trim().split('.') 22 | do { 23 | const key = keys.shift() 24 | if (key && data) { 25 | data = data[key] 26 | } 27 | else { 28 | return defaults 29 | } 30 | } while (keys.length) 31 | } 32 | return data === undefined 33 | ? defaults 34 | : data 35 | } 36 | 37 | function removeItem (arr, item) { 38 | const index = arr.indexOf(item) 39 | if (index > -1) { 40 | arr.splice(index, 1) 41 | } 42 | } 43 | 44 | function toArray (value) { 45 | return Array.isArray(value) 46 | ? value 47 | : [value] 48 | } 49 | 50 | function uniq (values) { 51 | return [...new Set(values)] 52 | } 53 | 54 | function wait (ms = 0) { 55 | return new Promise(function (resolve) { 56 | setTimeout(resolve, ms) 57 | }) 58 | } 59 | 60 | module.exports = { 61 | toSentence, 62 | toCamel, 63 | sortObject, 64 | getProperty, 65 | removeItem, 66 | toArray, 67 | uniq, 68 | wait, 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/package.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs') 2 | 3 | /** 4 | * Package.json info 5 | * 6 | * @typedef {Object} Package 7 | * @property {string} name The package name 8 | * @property {string[]} [workspaces] A list of workspaces 9 | * @property {Object.} [scripts] A list of scripts 10 | * @property {Object.} [dependencies] A list of dependencies 11 | * @property {Object.} [devDependencies] A list of dev dependencies 12 | * @property {Object.} [spaceman] Spaceman settings 13 | */ 14 | 15 | function isValidName (name) { 16 | const rx = /^[\da-z][-+.\da-z]+$/ 17 | return name.startsWith('@') 18 | ? name.substring(1).split('/').every(name => rx.test(name)) 19 | : rx.test(name) 20 | } 21 | 22 | function getManager () { 23 | return Fs.existsSync('./yarn.lock') 24 | ? 'yarn' 25 | : Fs.existsSync('./pnpm-lock.yaml') 26 | ? 'pnpm' 27 | : 'npm' 28 | } 29 | 30 | /** 31 | * Get a manager-specific install / remove / update command 32 | * 33 | * @param {string} task 34 | * @param {string} depType 35 | * @param {string} workspace 36 | * @param {string} deps 37 | * @returns {string} 38 | */ 39 | function getCommand (task = 'install', depType = '', workspace = '', deps = '') { 40 | const manager = getManager() 41 | const yarnOps = { 42 | install: 'add', 43 | uninstall: 'remove', 44 | update: 'upgrade', 45 | } 46 | const isYarn = manager === 'yarn' 47 | const op = isYarn 48 | ? yarnOps[task] 49 | : task 50 | const flag = task === 'install' 51 | ? isYarn 52 | ? depType 53 | ? `--${depType}` 54 | : '' 55 | : depType 56 | ? `--save-${depType}` 57 | : deps 58 | ? '--save' 59 | : '' 60 | : '' 61 | const command = isYarn 62 | ? workspace 63 | ? `workspace ${workspace} ${op}` 64 | : `${op}` 65 | : workspace 66 | ? `${op} --workspace=${workspace}` 67 | : `${op}` 68 | return `${manager} ${command} ${deps} ${flag}`.replace(/\s+/g, ' ').trim() 69 | } 70 | 71 | function getPackagePath (path = '', file = 'package.json') { 72 | return `.${path}/${file}` 73 | } 74 | 75 | function getScripts (path = '') { 76 | const scripts = readPackage(path).scripts 77 | return scripts 78 | ? Object.keys(scripts) 79 | : [] 80 | } 81 | 82 | function getDependencies (input) { 83 | const pkg = typeof input === 'string' 84 | ? readPackage(input) 85 | : input 86 | if (pkg) { 87 | return [ 88 | ...Object.keys(pkg.dependencies || {}), 89 | ...Object.keys(pkg.devDependencies || {}), 90 | ] 91 | } 92 | return [] 93 | } 94 | 95 | /** 96 | * Read package.json 97 | * 98 | * @param {string} path 99 | * @returns {Package|null} 100 | */ 101 | function readPackage (path = '') { 102 | path = getPackagePath(path) 103 | try { 104 | const text = Fs.readFileSync(path, 'utf-8') 105 | return JSON.parse(text) 106 | } 107 | catch (err) { 108 | return null 109 | } 110 | } 111 | 112 | function writePackage (path = '', data) { 113 | path = getPackagePath(path) 114 | try { 115 | Fs.writeFileSync(path, JSON.stringify(data, null, ' ') + '\n', 'utf-8') 116 | } 117 | catch (err) { 118 | console.error(err) 119 | } 120 | } 121 | 122 | module.exports = { 123 | isValidName, 124 | getManager, 125 | getCommand, 126 | getScripts, 127 | getDependencies, 128 | readPackage, 129 | writePackage, 130 | } 131 | -------------------------------------------------------------------------------- /src/utils/shell.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs') 2 | 3 | function log (text) { 4 | console.log('» '.grey + text.red) 5 | } 6 | 7 | function exec (command, silent = false) { 8 | log(command) 9 | const res = shell.exec(command, { silent }) 10 | if (res.code !== 0) { 11 | console.log('\n' + res.stderr) 12 | shell.exit(0) 13 | } 14 | return res.stdout 15 | } 16 | 17 | function exit () { 18 | console.log() 19 | process.exit(0) 20 | } 21 | 22 | module.exports = { 23 | log, 24 | exec, 25 | exit, 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/spaceman.js: -------------------------------------------------------------------------------- 1 | const { readPackage } = require('./package') 2 | const { getProperty } = require('./index') 3 | 4 | /** 5 | * Get a Spaceman setting from package.json 6 | * 7 | * @param {string} setting 8 | * @param {*} defaults 9 | * @returns {string|{}} 10 | */ 11 | function getSetting (setting = '', defaults = undefined) { 12 | let settings = readPackage()?.spaceman || {} 13 | return getProperty(settings, setting, defaults) 14 | } 15 | 16 | module.exports = { 17 | getSetting, 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/vars.js: -------------------------------------------------------------------------------- 1 | const ROOT = '[root]'.grey 2 | 3 | module.exports = { 4 | ROOT 5 | } 6 | -------------------------------------------------------------------------------- /src/workspaces.js: -------------------------------------------------------------------------------- 1 | const Fs = require('fs') 2 | const { makeChoicesGroup } = require('./utils/enquirer') 3 | const { readPackage } = require('./utils/package') 4 | const { exit } = require('./utils/shell') 5 | const { ROOT } = require('./utils/vars') 6 | 7 | // --------------------------------------------------------------------------------------------------------------------- 8 | // functions 9 | // --------------------------------------------------------------------------------------------------------------------- 10 | 11 | /** 12 | * Package.json info 13 | * 14 | * @typedef {Object} Package 15 | * @property {string} name The package name 16 | * @property {string[]} [workspaces] A list of workspaces 17 | * @property {Object.} dependencies A list of dependencies 18 | * @property {Object.} devDependencies A list of dev dependencies 19 | */ 20 | 21 | /** 22 | * Workspace info 23 | * 24 | * @typedef {Object} Workspace 25 | * @property {string} name The package name, e.g. tools or @web/tools 26 | * @property {string} group The workspace group, e.g. apps 27 | * @property {string} folder The workspace folder, e.g. web 28 | * @property {string} path The workspace path, e.g. apps/web 29 | */ 30 | 31 | /** 32 | * Get groups within a workspace 33 | * 34 | * @returns {string[]} 35 | */ 36 | function getWorkspaceGroups () { 37 | const workspaces = readPackage().workspaces 38 | return workspaces 39 | ? workspaces 40 | .filter(group => group.endsWith('*')) 41 | .map(group => group.replace('/*', '')) 42 | : [] 43 | } 44 | 45 | /** 46 | * Get folders within a workspace group 47 | * 48 | * @param {string} group 49 | * @returns {string[]} 50 | */ 51 | function getWorkspaceGroupFolders (group) { 52 | return Fs 53 | .readdirSync(`./${group}`, { withFileTypes: true }) 54 | .filter(entry => entry.isDirectory()) 55 | .map(entry => entry.name) 56 | } 57 | 58 | /** 59 | * Get a workspace by value 60 | * 61 | * @param {string} value 62 | * @param {string} key 63 | * @returns {Workspace|null} 64 | */ 65 | function getWorkspace (value, key = 'name') { 66 | return workspaces.find(workspace => workspace[key] === value) || null 67 | } 68 | 69 | /** 70 | * Get information about a workspace 71 | * 72 | * @param {string} folder 73 | * @param {string} group 74 | * @returns {Workspace} 75 | */ 76 | function getWorkspaceInfo (folder, group = '') { 77 | const path = group 78 | ? `/${group}/${folder}` 79 | : `/${folder}` 80 | const pkg = readPackage(path) 81 | if (pkg) { 82 | const name = pkg.name 83 | return { 84 | name, 85 | folder, 86 | group, 87 | path, 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Helper function to get the relative path of a workspace 94 | * 95 | * This function factors out the special ROOT group value 96 | * 97 | * @param {string} group 98 | * @param {string} folder 99 | * @returns {string} 100 | */ 101 | function getWorkspacePath (group, folder) { 102 | return group === ROOT 103 | ? `/${folder}` 104 | : `/${group}/${folder}` 105 | } 106 | 107 | /** 108 | * Gets all workspaces 109 | * 110 | * @returns {Workspace[]} 111 | */ 112 | function getWorkspaces () { 113 | return workspaces 114 | } 115 | 116 | /** 117 | * Gets an Enquirer choices from a source set of workspaces 118 | * 119 | * @param {Workspace[]} spaces 120 | * @returns {{role: string, choices: *, value: *}[]} 121 | */ 122 | function getWorkspacesChoices (spaces = workspaces) { 123 | const groups = {} 124 | const root = [] 125 | for (const workspace of spaces) { 126 | const { group, folder, name } = workspace 127 | if (!group) { 128 | root.push(folder) 129 | continue 130 | } 131 | if (!groups[group]) { 132 | groups[group] = [] 133 | } 134 | groups[group].push(name) 135 | } 136 | const choices = Object.keys(groups).sort().map(group => { 137 | return makeChoicesGroup(group, groups[group]) 138 | }) 139 | if (root.length) { 140 | choices.push(makeChoicesGroup(ROOT, root)) 141 | } 142 | return choices 143 | } 144 | 145 | // --------------------------------------------------------------------------------------------------------------------- 146 | // init 147 | // --------------------------------------------------------------------------------------------------------------------- 148 | 149 | const pkg = readPackage() 150 | 151 | if (!pkg) { 152 | console.log('No package file in this folder') 153 | exit() 154 | } 155 | 156 | if (!pkg.workspaces) { 157 | console.log('No workspaces in this package') 158 | exit() 159 | } 160 | 161 | const workspaces = pkg.workspaces 162 | .map(ref => { 163 | if (ref.endsWith('/*')) { 164 | const group = ref.replace('/*', '') 165 | if (Fs.existsSync(group)) { 166 | return getWorkspaceGroupFolders(group) 167 | .map(folder => getWorkspaceInfo(folder, group)) 168 | } 169 | } 170 | else { 171 | return getWorkspaceInfo(ref) 172 | } 173 | }) 174 | .flat() 175 | .filter(e => e) 176 | 177 | // --------------------------------------------------------------------------------------------------------------------- 178 | // exports 179 | // --------------------------------------------------------------------------------------------------------------------- 180 | 181 | module.exports = { 182 | getWorkspaces, 183 | getWorkspacesChoices, 184 | getWorkspace, 185 | getWorkspacePath, 186 | getWorkspaceGroupFolders, 187 | getWorkspaceGroups, 188 | } 189 | --------------------------------------------------------------------------------