@dileep-ct/grove
v1.5.0
Published
Run every branch of a Laravel app side by side: one git worktree per branch with its own URLs, databases, dependencies and logs (php-fpm with nginx or Apache, or Laravel Sail; MySQL/MariaDB or SQLite; Linux and macOS; optional HTTPS)
Maintainers
Readme
grove
Run every branch of your Laravel app side by side. grove gives each branch its own git worktree
with its own URLs, its own copy of the databases, its own vendor/ and node_modules/, .env and
logs. Review a pull request, try a migration or keep a long feature branch open while your main
checkout stays exactly as it was: no stashing, no re-seeding, no switching back and forth.
grove new feat/5000 → ~/www/my-app-worktrees/feat-5000
http://feat-5000.my-app.test, http://acme.feat-5000.my-app.test, …
databases grove_feat_5000 + grove_feat_5000__<tenant-id> per tenantFeatures
- A worktree per branch in a folder next to your checkout, created from an existing branch, a remote
branch or a new one.
grove cd feat-5000takes you there. - Its own URLs, served by nginx or Apache with php-fpm, or with Laravel Sail by its own app container in Docker, over HTTP or HTTPS (with mkcert certificates), including one per tenant subdomain.
- Its own databases, cloned from the main checkout or another worktree: MySQL/MariaDB with
mysqldump (stancl/tenancy tenant databases too), or SQLite with
VACUUM INTO. Or share the main checkout's with--db shared. - Fast dependencies:
vendor/andnode_modules/are copied copy-on-write where the filesystem allows, and composer or npm only run when the lock files differ from the main checkout's. - A ready
.env, generated from the main checkout's, pointing at the worktree's own URL, databases, cache, session cookie and logs, plus a "grove keep" block for your own per-worktree settings. - Migrations and the asset build run for you; every step is logged, and an interrupted
grove newcontinues where it stopped. - Housekeeping:
grove listshows activity and push/merge state,grove staleandgrove prunefind and remove idle worktrees safely,grove rmbacks up uncommitted work and deletes a branch only when it is merged, andgrove doctorfinds anything left behind. - Shell integration for bash, zsh and fish:
grove cdand tab completion. - Linux and macOS.
Requirements
- Linux or macOS. On Linux, grove knows the packages, web server layouts and php-fpm sockets of the Arch, Debian/Ubuntu, Fedora/RHEL and openSUSE families; on macOS it uses Homebrew. WSL is not tested.
- Node 20.12+, git and flock (util-linux on Linux;
brew install flockon macOS). - One way to run the app (grove calls it the runtime; see Runtimes):
- php-fpm on the machine (
host): php with php-fpm, composer, npm (or the pnpm, yarn or bun your app uses) and nginx or Apache. MySQL apps need the mysql/mariadb client tools, SQLite apps PHP's pdo_sqlite. - Laravel Sail (
sail): Docker with its compose plugin (Docker Desktop, OrbStack or Colima on macOS). PHP, Composer, Node and the database client all come from Sail's containers. MySQL/MariaDB apps only, so far.
- php-fpm on the machine (
- sudo, for the small root helper that writes
/etc/hostsentries (and, with php-fpm, the worktrees' web server sites). - For HTTPS: mkcert (and certutil, so Firefox trusts it too).
grove setup lists anything missing and offers to install it with your package manager (apt, pacman,
dnf, zypper or Homebrew).
Install
npm install -g @dileep-ct/groveUpdate with npm install -g @dileep-ct/grove@latest, then run grove setup once if grove asks for it.
Install grove globally, not in your app: it is a machine-wide tool, and its root helper and shell
integration expect one grove per machine.
Quick start
cd ~/www/my-app
grove init # once per repository: writes .grove.json; commit it
grove setup --passwordless # once per machine and repository (asks for your sudo password)
grove new feat/123 # create the worktree, then open the URL it prints
grove cd feat-123 # work in it
grove rm feat-123 # remove it and everything it ownsRun grove inside the app's checkout or one of its worktrees; from anywhere else it uses the checkout
it worked with last (GROVE_MAIN=<path> picks one explicitly). grove help lists every command.
With Laravel Sail, start Sail in the main checkout first (./vendor/bin/sail up -d); the rest is the
same.
What grove new does
Each step is recorded in the worktree's manifest, so running grove new again after an error or
Ctrl-C continues where it stopped.
- worktree:
git worktree addfor the branch, which it creates from--from(default<remote>/<baseBranch>) when it does not exist yet. - deps: copies
vendor/andnode_modules/from the main checkout, copy-on-write where the filesystem supports it (--fresh-depsinstalls them from the lock files instead). - files: copies the git-ignored files listed in
copyIgnored, such as.env.testing. - env: writes the worktree's
.env(see The worktree's .env). - databases: clones the databases (and tenant databases), or uses the main checkout's with
--db shared. - storage: an empty
storage/logs, the tenants' storage folders, thepublic/storagelink. - permissions: lets php-fpm's user write
storage/andbootstrap/cache/(with ACLs when php-fpm runs as another user, as on Debian/Ubuntu; Sail needs none). - container (Sail): starts the worktree's own app container (see Laravel Sail).
- composer: only
composer dump-autoloadwhenvendor/was copied andcomposer.lockmatches the main checkout's, elsecomposer install. When PHP doesn't satisfycomposer.lock, it offers to retry with--ignore-platform-reqs(setGROVE_COMPOSER_FLAGSto always use it). - npm: installs the JavaScript dependencies (with npm, pnpm, yarn or bun) when no
node_modules/was copied or the lock file differs from the main checkout's; otherwise the copy is used as is. - migrate:
php artisan migrate --step, andtenants:migratefor stancl/tenancy. - assets: the
buildcommand,npm run buildby default (a failure only warns). - web: the web server site (with Sail, a route in grove-router) and
/etc/hostsentries for the worktree and its tenants.
With Sail, composer, npm, artisan and the build run inside the worktree's app container. It ends with a summary: the URLs with their HTTP status, the databases, and what to do next.
Commands
Most commands take -y/--yes (do not ask) and --offline (do not fetch). Where a command takes
[name], it defaults to the worktree you are in; a branch name works too.
Setting up
| Command | What it does |
| --- | --- |
| grove init [--force] | Detects the app (Laravel; asks which database) and writes .grove.json, the repository's grove settings, to commit for the whole team. --force detects everything again. |
| grove setup [--passwordless] [--check] | Installs the root helper and the web server include (asks for your sudo password), plus completions and grove cd for bash, zsh and fish; with Sail, starts grove-router instead of using a web server. Offers to install missing tools. --passwordless lets your user run only grove's root helper without a password; --check only reports. |
| grove setup --user | Only the per-user parts, without sudo: the ~/.local/bin/grove link and the shell integration. |
| grove config | Shows the effective settings and where they come from. |
| grove config --https / --http | Serves the worktrees over HTTPS with mkcert certificates, or back over plain HTTP: saves GROVE_SCHEME, offers to install mkcert if needed, runs grove setup, and offers to update the worktrees' .env. |
| grove config --runtime host\|sail\|auto | Runs new worktrees with php-fpm on this machine or with Laravel Sail, or lets grove detect it from .env (auto, the default): saves GROVE_RUNTIME, then runs grove setup. Existing worktrees keep the runtime they were made with. |
Worktrees
| Command | What it does |
| --- | --- |
| grove new <branch> [options] | Creates a worktree, or continues one that stopped halfway. |
| grove list [--fetch] [--json] | Every worktree with its activity, uncommitted work, and push and merge state. |
| grove info [name] | Everything about one worktree. |
| grove rm [name] [options] | Removes a worktree and everything it owns: folder, databases, web server site (or Sail container and route), /etc/hosts entries, cache keys, and its branch when merged. |
| grove stale [--days N] | Worktrees idle for N+ days (default 30), rated SAFE (nothing would be lost), UNMERGED (all pushed, not merged) or AT-RISK (work only on this machine), plus every worktree holding work that is not on the remote. |
| grove prune [--days N] [--dry-run] | Removes the idle worktrees rated SAFE, after asking. |
| grove doctor [--fix] | Checks the setup and every worktree, and finds databases, web server sites, Sail containers and routes, and git worktrees that no worktree owns; --fix offers to remove them. |
Remove worktrees with grove rm, not by deleting the folder: the folder is only part of a worktree.
Deleted a worktree folder by hand? git still counts its branch as checked out there, and its databases (MySQL/MariaDB), site, names and container are still there. grove notices and tells you:
grove new <branch>makes git forget a deleted folder that holds the branch, then creates the worktree as usual. For a grove worktree, it offers to rebuild the folder instead, keeping its MySQL databases (a SQLite copy lived in the folder, so it is copied again).grove rm <name>removes whatever is left, as with any worktree.grove list,grove infoandgrove doctorpoint out deleted folders, and commands that need the folder (cd,run,dev,test,env,web,db:reset,deps:reset) say how to fix it instead of failing.grove doctor --fixmakes git forget deleted folders that grove did not make.
Options for grove new:
| Option | |
| --- | --- |
| --from <ref> | Base for a branch that does not exist yet (default <remote>/<baseBranch>; without it, grove asks: that, your current branch, or a branch, tag or commit you type, where a branch name means the remote's branch first). |
| --name <name> | Folder and URL name (default: the branch, with / → -). |
| --db clone\|shared | clone (default) copies the databases; shared uses the main checkout's. |
| --db-from main\|<name> | Clone from the main checkout (default) or another worktree. |
| --no-migrate, --no-build | Skip the migrations or the asset build. |
| --fresh-deps | Install vendor/ and node_modules/ from the lock files instead of copying them. |
| --no-web | No site, route or names (no URLs); add them later with grove web <name>. |
| --open | Open the first tenant (or the app) in the browser when done. |
Options for grove rm:
| Option | |
| --- | --- |
| --force | Remove even with uncommitted work (backed up first) or processes running in the folder. |
| --keep-branch, --delete-branch | By default the branch is deleted only when it is verified merged. |
| --backup-db | Dump the databases before dropping them. |
Working in a worktree
| Command | What it does |
| --- | --- |
| grove cd [name] | Changes into it (needs the shell integration from grove setup). |
| grove path [name] | Prints its folder. |
| grove url [name] [tenant\|central] | Prints a URL; grove open opens it in the browser. |
| grove run <name> -- <command…> | Runs a command in it, e.g. grove run feat-5000 -- php artisan tinker; with Sail, in its app container. |
| grove dev [name] | npm run dev (Vite picks a free port per worktree; with Sail it runs in the container, on the port grove gave it). |
| grove test [name] <file\|--filter=…\|--all> [args…] | php artisan test --compact, one worktree at a time, since they share the testing database; the whole suite only with --all. |
| grove env [name] | Regenerates its .env from the main checkout's, keeping the "grove keep" block. |
| grove db:reset [name] [--from main\|<name>] [--no-migrate] | Clones the databases again and migrates. |
| grove deps:reset [name] [--fresh] | Replaces vendor/ and node_modules/ with clean copies. |
| grove web [name] | (Re)creates its web server site (or route) and /etc/hosts entries; run it after adding tenants in it. With Sail it also re-creates its app container when it is missing or the main app runs a newer image (after sail build). |
Configuration
.grove.json
grove init reads the checkout once and proposes this file. It asks which database the app uses (the
one in .env's DB_CONNECTION is preselected) and, when APP_URL is localhost, which domain the
worktrees get. After that every command follows the file. Commit it, so your whole team gets the same
settings. (The runtime is the one thing each machine decides for itself, from its own .env, unless
the file pins it.)
{
"profile": "laravel",
"database": "mysql", // "mysql" (MySQL or MariaDB) or "sqlite"
"tenancy": "none", // or (MySQL only) { "driver": "stancl", "prefixEnv": "TENANCY_TENANT_PREFIX", "defaultPrefix": "tenant-" }
"domain": "my-app.test", // optional: default is APP_HOST, else APP_URL's host, from .env
"runtime": "sail", // optional: "host" or "sail" for everyone; default: each machine detects it
"baseBranch": "main", // new branches start from <remote>/<baseBranch>
"packageManager": "npm", // npm | pnpm | yarn | bun | none
"build": "npm run build", // "" for no asset build
"copyIgnored": [".env.testing"], // git-ignored files each worktree gets a copy of
"skipDataTables": ["jobs", "cache"] // tables cloned without their rows
}Personal settings
Each developer can override .grove.json and set more in ~/.config/grove/config (KEY=value
lines) or as GROVE_* environment variables; a value in the file wins over the environment variable.
grove config shows the effective values.
| Setting | Default | |
| --- | --- | --- |
| GROVE_ROOT | <checkout>-worktrees, next to the checkout | Where worktrees are created. |
| GROVE_DOMAIN | domain in .grove.json, else from .env | The domain worktrees get subdomains of. |
| GROVE_SCHEME | http | https serves the worktrees with mkcert certificates (set it with grove config --https). |
| GROVE_RUNTIME | detected | host (php-fpm) or sail; set it with grove config --runtime. |
| GROVE_WEB | detected | nginx or apache; detected from the web server that serves the main checkout. |
| GROVE_HTTP_PORT, GROVE_HTTPS_PORT | 80, 443 | Sail: the ports grove-router publishes; other ports end up in the URLs. |
| GROVE_FPM | detected | php-fpm's address, unix:/path/to.sock or 127.0.0.1:9000. |
| GROVE_FPM_USER | detected | The user php-fpm runs as, for storage/ permissions. |
| GROVE_REMOTE | origin | The git remote. |
| GROVE_BASE_REF | <remote>/<baseBranch> | Where new branches start. |
| GROVE_BUILD_CMD | build in .grove.json | The asset build command. |
| GROVE_COMPOSER_FLAGS | none | Extra composer install flags, e.g. --ignore-platform-reqs. |
| GROVE_COPY_IGNORED | copyIgnored in .grove.json | Space-separated. |
| GROVE_SKIP_DATA_TABLES | skipDataTables in .grove.json | Space-separated. |
| GROVE_MIN_FREE_GB | 5 | Free disk space grove new keeps, on top of what the worktree needs. |
| GROVE_STALE_DAYS | 30 | The idle threshold for grove stale and grove prune. |
| GROVE_MAIN | | Environment only: which checkout to use when run outside one. |
The worktree's .env
grove new and grove env write the worktree's .env from the main checkout's, changing only what
must differ per worktree:
APP_URL(andAPP_HOST, if your app uses it) → the worktree's URLDB_DATABASE→ its own database, and stancl's tenant prefix variable → its own tenant prefixCACHE_PREFIX,SESSION_COOKIEandHORIZON_PREFIX→ its own, so worktrees do not share cache entries or log each other outSESSION_DOMAINandSANCTUM_STATEFUL_DOMAINS→ with the worktree's domainLOG_CHANNEL=daily
Settings of your own go between the "grove keep" markers at the end of the file; they survive
grove env and win over everything above them:
# >>> grove keep: per-worktree settings >>>
MAIL_HOST=mailpit
# <<< grove keep <<<Runtimes: php-fpm or Laravel Sail
grove runs a worktree the way the main checkout runs:
- host: php-fpm on this machine, behind nginx or Apache. grove adds a site per worktree to the web server, and runs composer, npm and artisan on the machine.
- sail: Laravel Sail's Docker Compose stack. Each worktree gets its own app container, and grove-router routes the names.
By default each machine detects it from its own .env: Sail when the app has Laravel Sail and a Compose
file, and DB_HOST (or REDIS_HOST) names one of its services, as Sail's .env does (DB_HOST=mysql).
So teammates on Sail and on php-fpm can share one .grove.json. grove config shows what was detected
and why; grove config --runtime sail (or host) decides for your machine, and "runtime" in
.grove.json for everyone. Each worktree keeps the runtime it was made with.
Laravel Sail
Each worktree gets the same isolation as with php-fpm: its own folder, URLs, databases, vendor/,
node_modules/, .env, cache prefix and logs. What changes is where PHP runs:
- An app container per worktree.
grove newstartsgrove-<repo>-<name>, a copy of the main checkout's app container (laravel.test, orAPP_SERVICE): the same image, environment, extra hosts, networks and mounts, but with the worktree's folder mounted at/var/www/html. It shares the main checkout's MySQL, Redis, Mailpit and other services, and restarts with Docker. composer, npm, artisan,grove run,grove testandgrove devrun inside it, as Sail'ssailuser, so files stay yours. - grove-router is one small nginx container for the whole machine (
nginx:stable-alpine) that publishes ports 80 (and 443 with HTTPS) on 127.0.0.1 and sends each name to its container:feat-123.my-app.testand*.feat-123.my-app.testto the worktree's, the main checkout's domain (and itsAPP_URLhost, such aslocalhost) to the main app container. Its configuration lives in~/.local/state/grove/router/, so routes change without root. - Port 80: Sail's app publishes it itself by default (
APP_PORT=80).grove setupoffers to move the main app to another port (it setsAPP_PORTin the main.envand restarts the app container); the main app stays reachable at its usual names through grove-router. If you would rather keep port 80 for something else, setGROVE_HTTP_PORT(andGROVE_HTTPS_PORT) and the worktree URLs include that port. - Databases: grove clones them inside Sail's database container with its own mysqldump, connecting
as root (Sail sets the root password from
DB_PASSWORD), and lets Sail's database user (DB_USERNAME) use thegrove_…databases..envkeepsDB_HOST=mysql; onlyDB_DATABASEchanges, as before. - Vite: each worktree's container publishes its own port (5174 and up) and sets
VITE_PORT, which laravel-vite-plugin listens on under Sail;grove devprints it. - After
sail build, rungrove web <name>to move a worktree to the new image. - HTTPS: grove-router serves the mkcert certificates and hands requests to the containers as plain
HTTP with
X-Forwarded-Proto: https, so the app must trust proxies to see HTTPS (Laravel 11+:$middleware->trustProxies(at: '*')inbootstrap/app.php).
Sail must be running in the main checkout (./vendor/bin/sail up -d) for grove setup, grove new
and anything that touches the databases. sail down leaves the worktree containers and grove-router
running (Compose keeps its network while they use it); the worktrees answer again once Sail is back.
macOS
grove works on macOS with either runtime:
- Sail needs only Docker (Docker Desktop, OrbStack or Colima), Node, git and
brew install flock. - php-fpm uses Homebrew's nginx (its
servers/folder, so Laravel Valet's nginx works too) or Apache (Homebrew's httpd, or the one macOS comes with), and Homebrew's php-fpm (127.0.0.1:9000) or Valet's socket. The worktree sites live in/etc/grove/sites/<repo>/(nginx) or/etc/apache2/grove/<repo>/(Apache). Homebrew's nginx and httpd often run as you; if so, run them as root (sudo brew services start nginx, as Valet does), since grove's root helper tests and reloads their configuration as root. With HTTPS, the certificates belong to you, so a web server running as you can read them. vendor/andnode_modules/are APFS clones (cp -c), instant and sharing disk space, when the worktrees are on the same volume as the checkout.- mkcert puts its CA in the System keychain;
/etc/hostschanges flush macOS's name cache. - The shell integration goes in
~/.zshrc(or~/.bash_profilefor bash), and works with macOS's bash 3.2.
Web server, HTTPS and names
nginx or Apache
grove serves the worktrees with the web server that serves your main checkout (nginx if it cannot
tell); set GROVE_WEB=nginx or GROVE_WEB=apache to choose, then run grove setup. Each worktree gets
a site for its domain and *. its domain, with PHP going to php-fpm.
With Apache, PHP goes to php-fpm through mod_proxy_fcgi (mod_php is not supported) and Laravel's
public/.htaccess does the routing. grove setup turns on the modules the sites need (proxy,
proxy_fcgi, rewrite, and ssl for HTTPS): with a2enmod on Debian/Ubuntu and openSUSE, by uncommenting
their LoadModule lines in httpd.conf on Arch and macOS, and on Fedora/RHEL by offering to install
mod_ssl.
HTTPS
Worktrees are served over plain HTTP until you run:
grove config --https # and `grove config --http` to go backThen each worktree gets a certificate from mkcert for its domain and its tenant subdomains
(feat-123.my-app.test and *.feat-123.my-app.test), the web server (or grove-router) serves it on
port 443, plain HTTP redirects to HTTPS, and APP_URL starts with https://.
grove config --https offers to install mkcert when it is missing (and certutil, which Firefox
needs), saves GROVE_SCHEME=https, then runs grove setup. That runs mkcert -install, so your system
and browsers trust mkcert's local CA, renews the sites of the worktrees you already have, and offers to
update their .env with the new URLs. Run it again to repair the setup, or with --http to switch
back. mkcert runs as you and its CA stays in your home folder; the root helper only receives each
worktree's certificate, which it keeps readable by root only in /etc/grove/certs/ (with Sail,
grove-router reads them from ~/.local/state/grove/router/certs/, readable by you only).
Worktree names
grove puts each worktree's names in /etc/hosts (feat-123.my-app.test, and one per tenant, like
acme.feat-123.my-app.test) through its root helper, so they resolve on any Linux or macOS system
without a DNS server. /etc/hosts has no wildcards, so a tenant you create inside a worktree later gets its name
with grove web <name>. The main checkout's own names (my-app.test and its tenants) are yours to set
up, as before grove.
sudo and --passwordless
Writing web server sites and /etc/hosts needs root, which grove gets through a small root helper,
/usr/local/sbin/grove-helper. It is a root-owned bash script that does only four things (add or
remove a worktree's site, set or remove its /etc/hosts block), checks every argument against a strict
pattern, runs with a fixed PATH, and takes the domain, folders and php-fpm address from the
root-owned /etc/grove/<repo>.conf that grove setup writes, never from the caller. With Sail it only
manages /etc/hosts.
Without --passwordless, sudo asks for your password whenever grove needs the helper (grove new,
grove web, grove rm). grove setup --passwordless adds /etc/sudoers.d/grove-<you>, which lets
your user run only that helper without a password; use it on your own machine, and especially when
grove runs without a terminal (scripts, editors, agents). It is per user, covers every repository, and
stays until you remove it.
Databases and tenancy
- MySQL/MariaDB: each worktree gets
grove_<name>, plusgrove_<name>__<tenant-id>per tenant, cloned with mysqldump using the main checkout's database credentials from.env. The database user needs to create and dropgrove\_%databases; grove checks its privileges before creating anything. Tables inskipDataTablesare cloned without their rows. With Sail, all of this happens inside the database container (see Laravel Sail). - SQLite: a copy of the database file at the same place inside the worktree, made with
VACUUM INTO, so it is consistent even while the app writes in WAL mode. - stancl/tenancy (MySQL): tenant databases are cloned and renamed into the worktree's namespace,
new tenants created inside a worktree get databases there too (through the prefix variable), and
tenants are reached at
<tenant>.<worktree>.<domain>, from the subdomain labels in thedomainstable. --db shared: the worktree uses the main checkout's databases, and grove runs no migrations.
Where things live
- Worktrees:
GROVE_ROOT, by default<checkout>-worktreesnext to the checkout. - State:
~/.local/state/grove/<repo>/: a manifest per worktree inworktrees/, logs inlogs/, backups of uncommitted work and dumped databases inbackups/. - Personal settings:
~/.config/grove/config. - Sail: the containers
grove-<repo>-<name>(their environment in~/.local/state/grove/<repo>/containers/) andgrove-router, whose routes and certificates are in~/.local/state/grove/router/. - Shell integration:
~/.local/bin/grove,~/.local/share/grove/(sourced from~/.bashrcand~/.zshrc, or~/.bash_profileon macOS) and~/.config/fish/{functions,completions}/grove.fish. - Root side:
/usr/local/sbin/grove-helper,/etc/grove/<repo>.conf,/etc/grove/certs/<repo>/, the sites in/etc/nginx/grove/<repo>/(or/etc/grove/sites/<repo>/on macOS,/etc/apache2/grove/<repo>/,/etc/httpd/grove/<repo>/), agrove-<repo>.confinclude in the web server's site folder, thegrove:<repo>:<name>blocks in/etc/hosts, and/etc/sudoers.d/grove-<you>with--passwordless.
Limitations
- Linux or macOS, with nginx or Apache and php-fpm, or with Laravel Sail: no mod_php, Caddy, Herd, WSL or Docker setups other than Sail (Valet works as Homebrew nginx with php-fpm).
- MySQL/MariaDB or SQLite (SQLite with php-fpm only): no PostgreSQL or SQL Server.
- Tenancy means stancl/tenancy with tenants identified by subdomain; tenants on their own domains are not mirrored.
- Worktrees still share some services with the main checkout: Redis (with Redis queues, the main
checkout's worker may pick up a worktree's jobs), queues and schedulers in general, S3 buckets, mail,
search indexes, websockets, secondary database connections and the
testingdatabase. If your.envsetsDB_URL, it overridesDB_DATABASE, so the worktree would use the main database: unset it.
Uninstall
Remove the worktrees first (grove rm <name>, or grove prune), then grove's files (and with Sail,
docker rm -f grove-router):
repo=my-app # the repository's folder name
sudo rm -rf /usr/local/sbin/grove-helper /etc/grove/$repo.conf /etc/grove/certs/$repo /etc/sudoers.d/grove-$USER \
/etc/nginx/grove/$repo /etc/nginx/sites-enabled/grove-$repo.conf /etc/nginx/conf.d/grove-$repo.conf
sudo systemctl reload nginx # with Apache: the same files under /etc/apache2 or /etc/httpd, then reload it
rm -rf ~/.local/bin/grove ~/.local/share/grove ~/.config/fish/functions/grove.fish \
~/.config/fish/completions/grove.fish ~/.local/state/grove ~/.config/grove
npm rm -g @dileep-ct/groveAlso delete the # grove shell integration line from ~/.bashrc and ~/.zshrc.
Layout
src/: the CLI (TypeScript, no runtime dependencies; it drives git, mysql, cp, composer, npm and docker)libexec/grove-helper: the only part that runs as root (through sudo). It stays a small bash script on purpose: a root-owned/bin/bashscript is safer to allow in sudoers than a Node program, whose interpreter may live in a user-writable folder such as~/.nvm.shell/:grove cdand completions for fish, bash and zsh, copied into place bygrove setup
Development
From a clone, npm ci installs and builds, and npm install -g . points grove at the clone.
npm run typecheck
npm test # Node 22.18+ (runs the TypeScript tests directly)
npm run buildTo release, run npm version patch (or minor/major), npm publish (it asks for your 2FA code) and
git push --follow-tags.
