├── .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 |
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 |
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