Browse the projects in your development folder and run their scripts from one interactive menu, without changing directories or remembering script names.
Dev Workspace CLI detects each project's package manager (npm, pnpm, Yarn or Bun) and runs scripts with it, on Windows, macOS and Linux.
- Features
- Installation
- Quick start
- Usage
- Package managers
- Direct mode
- Command-line options
- Aliases
- Configuration
- Uninstalling
- Security
- Contributing
- License
- One menu for every project: lists the projects in your workspace folder and the scripts defined in each
package.json. - The right package manager: runs each project's scripts with npm, pnpm, Yarn or Bun, detected from the project itself.
- Fast to navigate: type a few letters to find a project or script, recently used projects come first, and
dws web devruns a script in one command. Menus also work with the mouse, with buttons for every action. - Stays out of the way: scripts run in the project folder with full terminal output. Pressing Ctrl+C stops the script and brings you back to the menu.
- Short commands: start it with
dev-workspace,dws, or an alias of your choice. - Remembers your workspace: the workspace, recent projects and last scripts are stored in your user profile and survives updates and reinstalls.
- Cross-platform: tested on Windows, macOS and Linux with Node.js 22, 24 and 26.
Dev Workspace CLI requires Node.js 22.13 or newer in the 22.x line, or 23.5 and later. Node.js 20 is no longer supported: it reached its end of life in April 2026.
npm install --global @modulane/dev-workspace-cliThis installs two equivalent commands, dev-workspace and the shorter dws.
-
Run the CLI:
dws
-
On the first launch, choose the folder that contains your projects, for example:
~/projects/ ├── storefront/ │ └── package.json ├── billing-api/ │ └── package.json └── design-system/ └── package.jsonThe CLI lists the usual project folders that exist in your home folder (
~/dev,~/projects,~/code,~/srcand~/repos) and, on Windows, at the root of each drive (E:\dev), each with the number of projects in it. Started from inside a project, it offers the folder that holds it first. One click or Enter is often enough. For any other folder, pick … Enter another path, or, when none of them exist, the CLI asks for the path straight away.You can type the path as you would in a terminal:
~/projects, or a quoted path such as one copied with Explorer's Copy as path. The folder is saved as an absolute path, so the CLI opens it directly next time. -
Then choose the accent colour of the CLI: cyan (the default), blue, purple, magenta, pink, orange, teal, sky, lavender, coral, gold, or no colour (bold text). Each name is shown in its own colour, and moving the cursor previews it on the whole screen. Both answers are saved, so later launches ask neither;
dws --set-color <name>changes the colour at any time. -
Choose a project, then one of its scripts. With this
package.json:{ "scripts": { "dev": "node server.js", "build": "node build.js", "test": "node --test" } }the project menu offers
dev,buildandtest. The script runs in the project folder with its normal output. When it ends, the CLI reports whether it succeeded, and you can run it again or return to the script list. -
Next time, skip the menus:
dws storefront devruns that script directly.
| Key | Action |
|---|---|
| ↑ / ↓ | Move through a list, and up to the buttons |
| Page Up / Page Down | Move a page at a time |
| Home / End | Go to the first or last entry |
| Letters, digits, … | Search the list |
| Backspace | Delete the last character of the search |
| Esc | Clear the search; with no search, go back |
| Enter | Open the selected project or run the script |
| → | Run the selected script with arguments |
| Tab / Shift+Tab | Move to the buttons and back to the list |
| ← / → | Move between buttons |
| Ctrl+C | Stop a running script, or quit from a menu |
Escape goes back one screen: from a project to the project list, from the workspace list or path prompt to the project list, and from the project list it exits. While you are searching, it only clears the search. The help line under each menu shows what it does (esc back or esc exit).
Every menu opens with a small box that shows where you are: the name of the workspace folder, followed by the project you are in (◆ dev › web-app), the workspace folder, and in a project, its package manager on the right. The box also holds the buttons of the screen, on the right of the workspace line: ⚙ Workspace and ✕ Exit in the project list, ← Back and … Run with args in a project. The buttons sit just above the list: press ↑ on the first entry to reach the first button, ↑ again for the next one, and ↓ to come back (Tab and Shift+Tab work too, and ← and → move between buttons). The focused button is shown in reverse, or between brackets without colours, and Enter presses it. Each script's command is shown next to its name, in a column of its own, and tags such as last run sit at the right edge. When a list is longer than the window, ▲ 3 more and ▼ 12 more show how many entries are hidden above and below. The layout stays within 100 columns on wide terminals. Long workspace paths are shortened in the middle (C:Users…projectsweb), keeping the folders that tell them apart, and script commands are cut with … to fit, and emoji or CJK characters in folder names never break the layout.
The menus are drawn on the terminal's alternate screen, as less and vim do, so resizing the window redraws the whole screen cleanly and the menus never fill your scrollback. A script's output appears on the normal screen and stays there after you return to the menus.
Colour marks where you are: the accent colour (cyan unless you chose another one on the first launch or with dws --set-color <name>) for the selected entry, the current project in the header and your search, and a faint version of it for the header's border. Green, red and yellow are kept for results and warnings, so they are not offered as accents. Buttons take the accent too: on a grey background under the mouse, and reversed when focused with the keyboard. In a project, the last run tag of the script you ran last is in the accent, so you can find it again after moving the cursor. The Git branch of each project gets a softer colour (main, master and trunk in one, any other branch in another). The rest is dim text. The workspace and arguments prompts use the same colours as the menus. Everything still reads without colour: the selected entry keeps its ❯ marker, and group titles are in capitals.
The accents other than cyan, blue and magenta use the terminal's 256-colour palette, which every current terminal supports.
Colours are on in a terminal and off when output is redirected to a file or another program. Set NO_COLOR to turn them off, or FORCE_COLOR to turn them on anyway (FORCE_COLOR=0 turns them off). FORCE_COLOR wins when both are set, as in Node.js.
Start typing in the project or script list to filter it. The characters you type must appear in the name in the same order, but not necessarily next to each other: dsy finds design-system. The best matches come first, preferring names that start with what you typed and matches at the start of words, and the matched characters are underlined. Typing always goes to the search, and the buttons are never part of the list, so Enter never presses one by accident.
In terminals that report the mouse, you can also use it in every menu. One click opens a project, since ← Back undoes it. A script needs a deliberate click, so a stray one never starts a build: the script under the pointer shows two buttons at the end of its row, ▶ run and … run with args, divided by a thin line in a faint version of the accent colour, or click the script once to select it and again, or double-click, to run it. The entry under the pointer is marked ›. Every button in the header box can be clicked. Clicking ▲ n more or ▼ n more moves a page. The keyboard works exactly as before.
The scroll wheel alone is enough to get around: it moves as ↑ and ↓ do, so scrolling up past the first entry reaches the buttons, and pressing the wheel (middle-click) opens the selected entry or presses the focused button, wherever the pointer is. The hint under each menu mentions it. Only searching and the questions that ask you to type, such as the workspace path or script arguments, need the keyboard.
While a menu is open, the mouse is used by the menu instead of selecting text; hold Shift or Option, depending on the terminal, and drag to select text anyway. Mouse reporting and the alternate screen are switched off before a script runs and whenever the CLI exits. To use only the keyboard, run dws --no-mouse for one run, or dws --set-mouse off to keep it that way; dws --set-mouse on turns the mouse back on.
| Terminal | Mouse |
|---|---|
| Windows Terminal | Works |
| Classic Windows console | Works |
| VS Code integrated terminal | Works |
| macOS Terminal | Partial: clicks land slightly off-centre; › shows which entry a click hits |
| Git Bash | Off: Git Bash reports no mouse events |
| iTerm2, Linux terminals, others | Not checked yet |
In a terminal without full support, the first menu shows a short note, once: what does not work and how to get full mouse support, or, in a terminal not checked yet, a request for help in #26.
The three projects you used most recently are listed first, under Recent. In a project's script list, the cursor starts on the script you ran last, tagged last run. This history is kept in a separate state.json file next to the configuration (see Configuration); dws --clear-history deletes it without touching your settings.
A script runs on the normal screen, under the header box with the project and script as breadcrumb (◆ dev › web-app › build) and the command it runs (▶ npm run build). When it ends, a line says how it went and how long it took: ✔ build finished in 4.2s in green, or ✖ build failed · exit 1 · 0.8s in red, with the signal name (such as SIGINT) instead of the exit code when a signal stopped it.
Then ↻ Run again and ← Back to scripts sit side by side. Run again is focused, so pressing Enter, or r, repeats the script with the same arguments; Esc goes back to the list.
To pass arguments to a script, press → on it, or click its … run with args button, then type the arguments, for example --port 4000. The … Run with args button in the header box does the same after asking which script to run. Use quotes for an argument that contains spaces: --title "my app". The arguments are passed to the script as they are, without a shell. Press Esc at the arguments prompt to go back to the scripts without running anything.
Every subfolder of the workspace is listed as a project, including links to folders (symbolic links, and junctions on Windows), except folders that are never projects: names starting with a dot (such as .git or .vscode) and node_modules. Only the immediate subfolders are listed; projects nested deeper are not discovered.
Projects are sorted by name, ignoring case. Folders without a package.json have no scripts to run, so they appear dimmed at the end of the list, tagged no package.json, and cannot be selected. A package.json that starts with a UTF-8 byte order mark, as Windows PowerShell 5.1 and some editors write it, is read normally, as npm does.
Next to each project, the list shows how many scripts it has and its Git branch (12 scripts · main, or the short commit when nothing is checked out), with its package manager on the right. These details are read only for the projects in view, as they scroll into the window, so a large workspace still opens at once. They are kept for the rest of the session, and read again for a project after you open it. The branch is read from .git/HEAD, so Git does not need to be installed.
In the script list, each script's command is shown next to its name, shortened to fit the terminal. Lifecycle hooks are listed with their main script instead of separately: with prebuild, build and postbuild scripts, the list shows only build, with the command node build.js (+ prebuild, postbuild); when the command is shortened, the hooks are always kept. Running build runs its hooks too, as the package manager normally does. A hook without its main script, such as postinstall on its own, is listed like any other script. In Yarn 2+ projects, which never run these hooks, every hook is listed as a separate script so you can still run it.
Each project's scripts run with the package manager that project uses. It is chosen in this order:
-
the
packageManagerfield inpackage.json, for example"packageManager": "pnpm@9.1.0"; -
the lockfile in the project folder:
Lockfile Package manager pnpm-lock.yamlpnpm yarn.lockYarn bun.lock,bun.lockbBun package-lock.json,npm-shrinkwrap.jsonnpm -
npm, when neither is present.
The box at the top of the project screen shows the choice and the reason, for example pnpm · pnpm-lock.yaml. If that package manager is not installed, the CLI explains how to install it. For pnpm and Yarn it suggests corepack enable on Node.js 24 and earlier; Node.js 25 and later no longer include Corepack, so there it suggests npm install --global first.
Whether pre and post hooks run is decided by the package manager: npm, pnpm, Bun and Yarn 1 run them, while Yarn 2 and later ignore them. The CLI recognises Yarn 2+ from a packageManager field such as yarn@4.5.0, or from a yarn.lock that starts with a __metadata: block, and then lists the hooks separately instead of grouping them.
Script arguments are passed the way each package manager expects them: after -- for npm, directly after the script name for pnpm, Yarn and Bun. Either way, the script receives exactly the arguments you gave, including a -- among them.
Give a project, and optionally a script, to skip the menus:
dws web # open the scripts of the "web" project
dws web dev # run its "dev" script, then exit
dws web dev -- --port 4000 # pass everything after -- to the script- The project is found by its exact name first, then ignoring case, then by the start of its name, as long as only one project starts that way:
dws des buildrunsbuildindesign-system. If several projects match, the CLI lists them. - Any script in
package.jsoncan be run by name, includingpreandposthooks. - With a script, nothing is asked and the screen is not cleared, so direct mode also works in other scripts. The CLI exits with the script's exit code, or 128 plus the signal number if a signal stopped it, as shells do (130 after Ctrl+C). It exits with code 1 if it cannot start the script, for example when no workspace is saved yet.
- Without a script, the CLI needs a terminal to read keys from. When its input is redirected (
dws < file, or in a CI job), it exits with code 1 and a message saying how to run a script directly, instead of waiting for keys that never come.
dev-workspace [options] [project [script] [-- arguments]]
| Option | Description |
|---|---|
-w, --workspace <path> |
Use <path> as the workspace and save it |
--no-mouse |
Use only the keyboard in menus, for this run |
--set-mouse <on|off> |
Save whether menus use the mouse |
--set-color <name> |
Save the accent colour (see Colours) |
--config-path |
Print the location of the configuration file |
--clear-history |
Forget the recent projects and last scripts |
--add-alias <name> |
Create another command that starts the CLI |
--remove-alias <name> |
Delete an alias created with --add-alias |
--list-aliases |
List the commands that start the CLI |
--reset |
Delete the configuration, history and custom aliases |
-v, --version |
Print the version |
-h, --help |
Show the help (coloured as described in Colours) |
Besides dev-workspace and dws, you can create your own command names:
dws --add-alias dev
devAn alias is a small launcher file placed next to the dev-workspace command, in the folder where global commands are installed. It works immediately in every shell, including Command Prompt, PowerShell and Git Bash on Windows.
dws --list-aliaseslists every command, anddws --remove-alias <name>deletes one.- Names that are already commands on your system, such as
ls, are refused, so an alias never hides another program. - The CLI only lists or deletes launchers it created.
Aliases require a global installation. If that folder needs administrator rights (for example /usr/local/bin), run the command with the same rights you used to install the CLI.
The workspace path is saved in the standard configuration folder of your operating system:
| Platform | Location |
|---|---|
| Windows | %APPDATA%\dev-workspace-cli\config.json |
| macOS | ~/Library/Preferences/dev-workspace-cli/config.json |
| Linux | $XDG_CONFIG_HOME/dev-workspace-cli/config.json (defaults to ~/.config/dev-workspace-cli/config.json) |
dws --config-pathprints the exact path on your machine.- The
DEV_WORKSPACE_CONFIGenvironment variable points the CLI at a different file. - If you edit the file by hand, an editor that saves it with a UTF-8 byte order mark does no harm.
- The accent colour chosen on the first launch is saved as
"color":{ "workspace": "…", "color": "teal" }.dws --set-color <name>changes it. A file saved by a version without this setting keeps cyan and is never asked again. dws --set-mouse offsaves"mouse": falsein the file, so menus use only the keyboard:{ "workspace": "…", "mouse": false }.dws --set-mouse onsavestrue. Changing the workspace keeps these settings, and they can be saved before any workspace is chosen.- To switch workspaces, press the ⚙ Workspace button in the project list or run
dws --workspace <path>. The button shows the same folders as the first launch, with the current one taggedcurrent, and … Enter another path. In that list, and at the path prompt, Esc keeps the current workspace and goes back to the projects. If the saved workspace no longer exists, the CLI asks for a new one. - Versions before 0.3.0 stored the file in
~/.dev-workspace-cli/config.json. If that file exists, it is moved to the new location automatically.
The history of recent projects and last scripts, and which mouse notes were shown, is kept apart from the settings, in state.json in the same folder as config.json. With DEV_WORKSPACE_CONFIG, it is named after your file instead (dws.json keeps it in dws.state.json), so it never overwrites a state.json of your own. It is only a convenience: if it is missing or damaged, the CLI starts with an empty history. dws --clear-history deletes it.
The configuration, history and custom aliases are kept outside the package, so they remain after an uninstall. To remove everything:
dws --reset
npm uninstall --global @modulane/dev-workspace-cliScripts run with your user account's permissions, exactly as if you ran them yourself. Only run scripts from projects you trust.
Bug reports, feature ideas and pull requests are welcome. Issues labelled good first issue and help wanted are ready to pick up. Pull requests are checked with Prettier, ESLint and the test suite in CI. See CONTRIBUTING.md for the development setup, workflow, labels and release process, and the roadmap for what is planned before 1.0.0.