Save a file. Run your command. Every time.
stye is a lightweight CLI that watches files or folders and re-runs a command when something changes. It has a single runtime dependency (chalk) and runs on Node.js 18 or newer.
stye ./src "npm test"- Nothing to configure to get started. Point it at a path, give it a command.
- Sensible by default. Bursts of saves become one run, and
node_modules,.git,distand editor temp files are ignored. - You choose what happens mid-run. Restart the command, queue exactly one more run, or run everything concurrently.
- It stops what it starts. Stopping a run ends the whole process tree, not just the top process.
- Nothing hidden. stye began as a way to learn how file watchers work under the hood. Watching, debouncing, glob matching, process control and the recursive-watch fallback are all written in this repo on top of native Node.js modules.
Requires Node.js 18 or newer.
npm install -g @oxaccolades/styeOr from source:
git clone https://github.com/Accoladesio/stye
cd stye
npm install # also builds the project
npm link # makes `stye` available globallystye ./src "npm run build"Edit a file in src, and stye reacts:
[Change detected]: src/core/watcher.ts
[Running]: npm run build
...your command's output, streamed live...
[Done]: exit code 0 in 1.4s
A failed run ends with [Failed]: exit code 1 in 1.4s. Your command's stdout and stderr stream straight to the terminal, colours included.
stye [options] <path> <command...>
stye [options] -w <path> [-w <path>...] <command...>
stye [options] -- <command...> # paths/command come from the config file
Options go before the path or command; everything after them is the command. Quote the command if it contains shell characters such as && or |.
stye ./src "npm run build"
stye -w src -w tests -e ts,tsx --mode queue npm test
stye --ignore "*.{log,tmp}" . node server.js # restarts the server on change
stye --verbose --timestamps ./src "npm run build"Watching
| Option | Description |
|---|---|
-w, --watch <path> |
File or folder to watch. Repeat for several |
-e, --ext <list> |
Only react to these extensions, e.g. ts,js |
--include <glob> |
Only react to matching paths (repeatable) |
--ignore <glob> |
Never react to matching paths (repeatable) |
--no-default-ignore |
Also watch node_modules, .git, dist and editor temp files |
--fallback |
Use the built-in directory walker instead of native recursive watching |
Running
| Option | Description |
|---|---|
-d, --debounce <ms> |
Quiet period after the last change before running (default 300) |
-m, --mode <mode> |
restart (default), queue or concurrent |
--kill-timeout <ms> |
Wait this long after SIGTERM before SIGKILL (default 3000) |
Output and other
| Option | Description |
|---|---|
-q, --quiet / --verbose |
Only errors and failed runs / extra diagnostics |
--timestamps |
Prefix log lines with the time |
--no-color |
Disable colours (NO_COLOR is honoured too) |
--no-keys |
Disable the interactive shortcuts |
-c, --config <file> |
Read settings from a JSON file |
-h, --help / -v, --version |
Help / version |
What happens when a change arrives while the command is still running:
- restart (default): stops the previous run, and everything it spawned, then starts a fresh one. Best for servers and dev tools.
- queue: leaves the running command alone. Changes that arrive meanwhile trigger exactly one more run afterwards. Best for builds and tests you don't want interrupted.
- concurrent: every change starts a new run immediately.
Globs support *, **, ?, [abc], [!abc] and {a,b}.
- Without a
/the pattern matches any path segment:node_modules,*.log. - With a
/it is anchored to the watched folder:src/generated,src/**/*.tmp. Matching a folder covers everything inside it. --ignorewins over--includeand--ext. An explicitly named file is always watched, whatever the filters say.
r rerun now · c clear screen · q or Ctrl+C quit
Instead of flags, put settings in a JSON file and just run stye.
{
"watch": ["src", "tests"],
"command": "npm test",
"ext": ["ts"],
"ignore": ["src/generated", "*.snap"],
"debounce": 200,
"mode": "queue"
}Where stye looks (first match wins):
- the file passed with
--config <file> stye.config.jsonin the current folder- the
"stye"key in the current folder'spackage.json
Available keys: watch, command, include, ext, ignore, defaultIgnore, debounce, mode, killTimeout, quiet, verbose, timestamps, color, keys, fallback. Unknown keys are reported by name.
CLI flags override the config file per key (lists are replaced, not merged). If the config supplies the paths, pass the command after --: stye -- npm run lint.
- Recursive watching with a built-in fallback for platforms where
fs.watchcannot recurse (Node 18 on Linux). The fallback attaches one watcher per folder, follows folders being created or deleted, and never descends into ignored folders. - Debounced runs: a burst of saves triggers one run.
- Clean process control:
SIGTERM, thenSIGKILLafter the kill timeout, applied to the whole process tree (taskkill /Ton Windows). - Live output: stdout and stderr stream straight to your terminal, even when the command fails. Each run ends with its exit code and duration.
- Friendly errors: bad flags, bad config values or a missing path give a one-line message and a pointer to
stye --help, not a stack trace. - Clean shutdown: Ctrl+C or
SIGTERMstops the watchers and any running command.
| Code | Meaning |
|---|---|
0 |
Clean shutdown (Ctrl+C, q or SIGTERM), or --help / --version |
1 |
Unexpected runtime error |
2 |
Usage error: bad flag, bad config value, or missing path |
src/
├── index.ts entry point (wiring only)
├── cli/
│ ├── args.ts flag parsing
│ ├── config.ts config file loading, validation, option merging
│ └── help.ts help text and version
├── core/
│ ├── watcher.ts native watching, multiple targets
│ ├── fallback-watcher.ts directory-walking watcher
│ ├── debounce.ts
│ ├── ignore.ts include / ignore / extension filtering
│ └── runner.ts restart / queue / concurrent command runner
├── utils/
│ ├── glob.ts glob to RegExp
│ ├── logger.ts levels, timestamps, colour
│ ├── keys.ts interactive shortcuts
│ ├── shutdown.ts signal and error handling
│ └── errors.ts
└── types/index.ts
tests/ node:test suites (run with `npm test`)
npm run dev -- ./src "echo changed" # run from source with ts-node
npm run build # compile to dist/
npm test # run the test suiteContributions are welcome. Fork the repo, create a branch, make your change with tests, run npm test, and open a pull request describing what changed. See CHANGELOG.md for history.
MIT © 2025 Accoladesio. See LICENSE.