npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@crouton-kit/grove

v0.2.37

Published

Parallel project instance manager for declarative apply, labels, ready pools, rollout, and rollback

Readme

Grove

Grove manages parallel project instances with isolated slot-based ports. Each registered project defines ports as base + slot × offset; grove plant reserves a slot, then clones or copies the project, applies the slot configuration, installs dependencies, and runs the project setup script. Registry changes are serialized with ~/.grove/grove.lock, so concurrent plants cannot take the same slot.

Development

pnpm install
pnpm build
pnpm dev -- --help

Project configuration

grove setup reads .grove/config.json at a source repository root, validates its contract, and converges the machine registration without authoring repository files. grove register remains the lower-level primitive for custom or explicit registration updates. Composite workspaces can keep the definition in their owning repository and pass its path relative to the registered source root:

grove register /path/to/workspace --config northlight/.grove/config.json

The selected config path is stored in ~/.grove/grove.json. An optional setup.sh beside the selected config runs after the instance has been copied or cloned. Teardown runs when the config names teardownScript.

A version 1 config can define:

  • name and instancesDir
  • slot-based ports
  • aliases for instance directories
  • repos to clone into a composite instance
  • copyFromSource for untracked local configuration
  • patchPortsIn globs
  • substituteIn rules for per-slot values that are strings rather than ports
  • per-repository install commands
  • teardownScript
  • secrets, per-repository commands that materialize untracked configuration in the target
  • devCommand, an optional executable path relative to the target root (for example scripts/dev.sh or northlight/scripts/dev.sh)
  • lifecycle, an optional mapping of start, stop, status, and reset to non-empty argument arrays for devCommand
  • nameIsSlot, an optional boolean that requires instances to use their slot number as both name and directory name
  • stateCommand, an optional executable path with the same shape, giving the project a data-state layer

devCommand and stateCommand must each point to an existing regular executable file inside the project root. Grove validates them during registration, doctor checks, and dispatch; it does not guess a script path.

secrets takes the same { dir, cmds } shape as install and runs in the target after copyFromSource and before patchPortsIn, so a generated .env gets its ports rewritten exactly like a copied one. Unlike install, a failing secrets command aborts the plant — a missing secret otherwise surfaces much later as an unexplained runtime failure.

Secret environment files

Every dispatched command — development, lifecycle, state, the legacy init script, setup, teardown, secrets, and install — receives optional secret env files. Grove reads and merges them in this order, so the nearest applicable scope wins:

  1. ~/.grove/env — user scope for every project
  2. ~/.grove/env.d/<project>.env — project scope for every slot
  3. <target>/.grove/env — slot scope after the target exists

The legacy init script receives the user and project scopes, but not the slot scope because it creates the target. Every other dispatched command receives all three scopes. A missing file is skipped. Each non-blank, non-comment line is KEY=value; the value is literal after the first =, surrounding whitespace is trimmed, and one matching pair of surrounding single or double quotes is removed. There is no interpolation, export prefix, or inline-comment syntax. A malformed line, NUL byte, or GROVE_* key refuses the command and names its file and line. A repeated key in one file refuses and names both line numbers. Refusal messages never print a value. GROVE_* keys are reserved for Grove's derived context and are refused in every secret env file; Grove's own context values always win over inherited and secret-file values.

grove doctor reports every scope file for the source and each instance, including whether it is missing, its key count and names, or a parse error. It never prints a value. Grove never creates, copies, or rewrites <target>/.grove/env: the normal source copy excludes it, and copyFromSource, port patching, and substitutions skip it.

"secrets": [
  { "dir": "northlight/apps/core", "cmds": ["op inject -i .env.tpl -o .env"] }
]

substituteIn covers the identities grove cannot derive arithmetically. A port is base + slot × offset, but a reserved hostname or a namespace token is a name, and an instance that inherits the source's copy claims an external identity another instance already owns. Each rule names its own files, pattern, and slot-shaped value:

"substituteIn": [
  {
    "in": ["northlight/apps/core/env/crouter.*.env"],
    "find": "nl-core-g\\d+\\.ngrok\\.app",
    "replace": "nl-core-${machine}-g${slot}.ngrok.app"
  }
]

in is a glob list relative to the target root, find is a JavaScript regular expression applied globally to each matching file, and replace is its replacement template with ${slot} expanded to the slot number and ${machine} expanded to the machine handle. Capture groups ($1, $<name>) work as usual. Grove compiles find when it validates the config, so a bad pattern is a refusal naming the rule rather than a half-rewritten instance. It also requires the result of applying all applicable rules a second time to be unchanged; otherwise it refuses before rewriting any selected file. Rules run after patchPortsIn, and never against .grove/config.json itself.

Grove computes the slot cap from the declared ports: it is the largest slot N for which every port in slots 0 through N is distinct and at most 65535. Requested slots must be positive safe integers; grove register and grove setup print the cap, and grove plant refuses a slot above it. A project with no declared ports has no port-derived cap.

Slot 0 is not special-cased the way ports are: a rule that produces the value the source already holds simply rewrites nothing.

Code

An instance's code and its data state are separate choices. plant --code-from picks the code:

  • configured (the default) clones each repo's branch from the project config, so a plain grove plant is reproducible no matter what the source checkout is doing
  • @source clones each source repo at its exact current commit, including commits that were never pushed, and keeps the real remote as origin

--code-from @source refuses before any filesystem work when a source repo is dirty, missing, or cannot resolve HEAD — a half-source, half-configured instance is worse than being asked to commit first. It applies only to projects that declare repos; a project grove copies wholesale already gets the source tree.

The two intentional launches are grove plant <project> onboarding — configured branches at the baseline state, both defaults — and grove plant <project> feature --code-from @source --from @source, the code and data you are working on right now.

Apply and configuration drift

Every planted instance records its requested spec (codeFrom, state from, and labels) and a newest-first history of its last ten applied revisions. Each revision has a SHA-256 configHash, time, and the branch and commit in each configured repository. The newest revision is the current applied record. The hash is the canonical JSON of the validated source config: object keys are sorted recursively and arrays retain their declared order. Whitespace and object-key order therefore do not make an instance stale.

grove apply <project/instance> converges an existing checkout without cloning code or changing data state. It can also select a fleet as described in Labels, selectors, and fan-out. It reruns copyFromSource, secrets, patchPortsIn, substituteIn, install, and setup.sh, then records the current validated source config as applied. It refuses the source checkout and an instance being planted, uprooted, or restored. It marks an instance applying before setup without holding the registry lock during setup; if interrupted, re-run grove apply <project/instance> to complete it. It also refuses a source port contract that differs from the registered project ports (run grove register --update first), any configured repository that is not a Git checkout, and tracked repository changes; pass --force only when overwriting tracked changes is intended. Untracked files are not a refusal and copyFromSource may overwrite them.

Port patching and substitutions write a file only when its content changes, so a second apply leaves an already-patched file unchanged. A substitution replacement may match its own pattern when the second substitution is unchanged, such as a canonical final identity.

grove doctor reports built from older config — grove apply <project>/<name> when the current source config hash differs from the recorded one. grove list marks the same instance stale config; grove list --json includes each instance's spec, applied, and configStale fields.

Labels, selectors, and fan-out

Pass --label key=value repeatedly to grove plant to record labels on a new instance. Label keys must match [a-z0-9._-]+; values must be non-empty and cannot contain a comma. Change labels later with grove label <target> key=value ... [--rm key]. grove list prints labels, and grove list --json carries them in spec.labels.

The start, stop, status, reset, restore, apply, uproot, and label commands target one explicit <target> as before, instances matching every label pair with -l key=value[,key=value] [project], or every planted instance with --all [project]. Omit [project] only when the current directory is in a registered project or exactly one project is registered. A selector matching no instances refuses and names the selector. The source at slot 0 is never selected by -l or --all; name it explicitly only for commands that permit the source.

A selector or --all walks selected instances sequentially in slot order. Grove stops at the first non-zero exit and prints a summary showing each target that succeeded, failed, or never started. grove uproot -l ... and grove uproot --all require --force; explicit grove uproot <project/name> retains its confirmation prompt.

For restore, use grove restore <target> <ref> for one target and grove restore [project] <ref> -l ... or grove restore [project] <ref> --all for a set. For label, use grove label [project] key=value ... -l ... or grove label [project] key=value ... --all for a set.

Warm pool

grove pool <project> shows the number of ready instances, their slots, and the number of claimed instances. Ready means claimable: exactly what grove claim will hand out, so an in-progress plant is not counted ready. grove pool <project> --size N plants with configured code and baseline state until N completed instances carry grove.pool=ready. It only grows; a pool already at or above the requested size is left unchanged. An in-progress plant does count toward that size, so concurrent grove pool runs do not overshoot; an interrupted plant keeps counting until you remove it with grove uproot <project/name> --force.

grove claim <project> [--label key=value...] atomically takes the lowest-slot ready instance, removes grove.pool=ready, and adds the supplied labels. It prints the same --- grove-output --- JSON block as grove plant. When no ready instance exists, grow the pool with grove pool <project> --size N.

grove release <project/instance> [--force] returns a claimed instance to the pool. It refuses tracked or untracked changes in every configured repository, side branches with commits on no remote, changed extra worktrees, and checkouts detached on commits no remote holds. Grove fast-forwards configured branches, removes every extra worktree, deletes every local branch except each configured branch, resets the data state, applies the project configuration, drops all labels, and sets grove.pool=ready. --force runs git reset --hard and git clean -fd in every configured repository and allows release to discard the otherwise refused side branches and worktrees. An interruption before the final save leaves the releasing reservation and the instance's existing labels in place; re-run grove release <target> to finish it.

Rollout and rollback

grove rollout <project> moves every planted instance in the project forward in slot order. Use -l key=value[,key=value] to select matching labels or --all to state the default explicitly. Before touching an instance, rollout refuses if any configured repository has tracked changes. It fetches and fast-forwards every configured repository to its configured branch, runs apply, then runs lifecycle stop and start when both are declared. When lifecycle status is declared, it must exit 0. Rollout stops at the first failure, names that instance, and leaves later instances unstarted.

grove rollback <project/instance> is the single-instance escape hatch. It refuses tracked changes and requires at least two recorded revisions. Grove checks out every configured repository on the previous revision's recorded branch and commit, reruns apply, and records a new revision with rolledBackFrom set to the timestamp of the revision it replaced.

State

Ports, namespaces, and env files are an instance's identity; its database contents are state. stateCommand lets a project choose what state a new instance starts from and return a live instance to a known one. Grove owns the store, the ref grammar, and the schema gate; the project owns what its state actually is.

Grove invokes the executable with cwd at the target root and the same context environment devCommand gets. Non-zero exit fails the operation.

| invocation | contract | |---|---| | state.sh reset | bring this instance to the empty/migrated baseline | | state.sh capture <dir> | write everything constituting this instance's state into <dir> (grove creates it empty) | | state.sh restore <dir> | load a previously captured <dir> back into this instance | | state.sh fingerprint | print one opaque line identifying the schema generation |

One ref grammar is shared by plant --from and restore:

  • baseline — runs reset; this is the plant default and reproduces a project's pre-state-layer behavior
  • @<instance> — captured live from that instance at the moment of use; @source means the project source at slot 0
  • <name> — a snapshot in the store

Snapshots live in ~/.grove/states/<project>/<name>/, holding meta.json and whatever capture wrote into data/. They are project-scoped, so uprooting the instance a snapshot came from leaves the snapshot intact.

capture records the fingerprint output; restore runs fingerprint against the destination and refuses on mismatch, naming both values. --ignore-fingerprint overrides it.

Settings

~/.grove/settings.json holds machine-level settings. It is optional, has no writer — edit it by hand — and grove doctor validates it.

{ "version": 1, "machine": "my-machine", "killTmuxSessionOnStop": true }

machine is optional. It must match ^[a-z0-9-]{1,16}$; when omitted, Grove uses the short hostname lowercased with every other character removed, truncated to 16 characters. Grove passes the effective value as GROVE_MACHINE, uses it for ${machine} substitutions, and prints it in grove doctor.

killTmuxSessionOnStop (default false) makes grove stop <target> kill the target's tmux session after the project's stop verb exits 0. The kill runs last, so the verb's output is written first, and a failed kill warns rather than failing the command — the services genuinely stopped. A non-zero stop leaves the session alone, so the window showing why it failed survives.

Grove derives the session name as <project>-<slot>: northlight-3 for slot 3 and northlight-0 for the project source. grove open --json prints it as tmuxSession, and grove ui's o key switches to it. An unknown key, a wrong type, a version other than 1, or unparseable JSON is a refusal naming the file and the key — from grove doctor and from grove stop before it stops anything.

grove ui

grove ui [project] is a terminal UI over one project's slots — it shows the source at slot 0, every instance, and empty rows through the project's computed slot cap, limited to rows that fit in the terminal. When the terminal is too short for all of them, the table keeps the lowest slots that fit and ends with … N more slots not shown.

grove ui — fixture  pool 2 ready · 3 claimed  /Users/silasrhyneer/Code/cli/.grove-fixtures/ui-surfaces/source
 SLOT  NAME         BRANCH               SYNC        STATE       SERVICES
▸  0  (source)     main                 ↑0 ↓0                   app:65000 ○ api:63000 ○
   1  alpha        main                 ↑0 ↓0                   app:65100 ○ api:63100 ○
   2  beta         main                 ↑0 ↓0       stale       app:65200 ○ api:63200 ○
   3  3            main                 ↑0 ↓0       pool        app:65300 ○ api:63300 ○
   4  gamma        applying      Re-run with: grove apply fixture/gamma
   5  delta        main                 ↑0 ↓0       no-state    app:65500 ○ api:63500 ○

The title line carries the project's pool: how many instances are ready to claim and how many are claimed, by the same rule grove pool prints.

A composite project has several repos per slot, so each column aggregates them: BRANCH is the branch every repo agrees on or the literal word mixed; SYNC is ↑A ↓B summed across the repos that have an upstream, with a trailing ? when any repo lacks one or its state is unknown; marks a slot where any repo has tracked changes (dirty never counts untracked files). The detail pane under the table always shows the selected slot's per-repo truth. SERVICES is each declared port and whether something is listening on it, which every project that declares ports gets without cooperating.

STATE is the recorded intent a fleet is scanned for: no-state when the instance's data state was never applied, stale when it was built from an older source config, and pool when it carries grove.pool=ready and is claimable. An instance with a reservation replaces its whole row with the operation and the command that resolves it, so an interrupted plant, uproot, apply, restore, release, rollout, or rollback is visible without selecting the row. Every row is one line at any width: cells truncate, nothing wraps.

The detail pane adds the selected instance's intent — its requested code mode, state ref, and labels — and its applied record: the short config hash, the time, how many revisions are recorded, the revision it was rolled back from, and stale — grove apply <target> when the source config has moved on. The source at slot 0 has no registry entry, so it shows neither.

| key | action | |---|---| | /k, /j | move the cursor | | 09 | jump to one of the table's first ten rows | | o | switch to the slot's tmux session, creating it if absent, and exit | | p | plant the selected empty slot | | u | uproot the selected instance, after a y/n confirmation | | s S r t | the project's start, stop, reset, and status lifecycle verbs; r confirms first | | a A | grove apply the selected instance, and apply --force over tracked changes; both confirm | | e E | grove release the selected instance into the pool, deleting side branches and extra worktrees, and release --force also discarding repository changes; both confirm | | b | grove rollback the selected instance one revision, after a confirmation; dimmed under two recorded revisions | | l L | add labels, or remove them by key — each opens a one-line prompt; Enter runs, Esc cancels | | c | grove claim the project's lowest-slot ready instance | | P | grow the project's ready pool: a prompt for the size, then a confirmation | | R | git fetch every repo, then re-read | | ? | help · q or Esc quit |

Every action runs Grove's own CLI as a child and shows the resolved argv, a rising elapsed count, and the child's output in the pane below the table; Ctrl-C interrupts it and leaves the UI running, and the inventory is re-read when it exits. Actions act on one target: c and P on the project, everything else on the selected row. grove ui has no rollout and no selector fan-out — use grove rollout and -l/--all from the command line for those.

A key the selected row does not support is dimmed in the footer and answers with the same refusal Grove's own command would print — p on an occupied slot, u or a on the source, or a lifecycle role the project's lifecycle mapping does not declare. A prompt refuses the same way before it confirms anything: a --size that is not a non-negative integer, or an option typed where it expects labels.

t runs the project's status verb and shows its output verbatim in the detail pane. Grove never parses it, so a project can print whatever it likes.

The project comes from the [project] argument, else the registered project containing the current directory, else the only registered project, else a picker. grove ui needs a terminal on both stdin and stdout and refuses when either is redirected — for a machine-readable inventory, grove list --json is the same data.

Commands

grove setup [source]
grove register <source> [--config <relative-path>] [--update]
grove dev [--at <target>] [raw argv...]
grove plant <project> [name] [--slot <n>] [--code-from <mode>] [--from <ref>] [--ignore-fingerprint] [--label <key=value>...]
grove apply [target-or-project] [-l <key=value[,key=value]> | --all] [--force]
grove pool <project> [--size N]
grove claim <project> [--label <key=value>...]
grove release <target> [--force]
grove rollout <project> [-l <key=value[,key=value]> | --all]
grove rollback <project/instance>
grove adopt <project> <name> <path> [--slot <n>]
grove list [project] [--json]
grove open [target] [--json]
grove start [target-or-project] [-l <key=value[,key=value]> | --all]
grove stop [target-or-project] [-l <key=value[,key=value]> | --all]
grove status [target-or-project] [-l <key=value[,key=value]> | --all]
grove reset [target-or-project] [-l <key=value[,key=value]> | --all]
grove label [target-or-project] [key=value...] [-l <key=value[,key=value]> | --all] [--rm <key>...]
grove doctor [project]
grove ui [project]
grove uproot [target-or-project] [-l <key=value[,key=value]> | --all] [--force]
grove snapshot <project/instance> <name> [--force]
grove restore [target-or-project] [ref] [-l <key=value[,key=value]> | --all] [--force] [--ignore-fingerprint]
grove states [project] [--rm <name>]

grove reset <target> runs the project's own lifecycle reset from its lifecycle mapping; grove restore <target> baseline runs the data-state reset through stateCommand. They are different operations — one returns the working environment to a known state, the other returns the database to its baseline.

grove dev resolves the current directory to the longest containing registered source or instance, then directly runs that target's configured devCommand. Arguments are forwarded unchanged, and Grove supplies GROVE_MACHINE, GROVE_SOURCE, GROVE_TARGET, GROVE_SLOT, GROVE_INSTANCE_NAME, GROVE_PORTS_JSON, and GROVE_PORT_<NAME> environment variables.

Teardown alone also receives GROVE_SIBLINGS_JSON: a JSON array of the project entries remaining after the instance being uprooted is removed, with objects shaped as { "name": string, "slot": number, "path": string }, sorted by slot. The source is always included as { "name": "<project>", "slot": 0, "path": "<registered source path>" }; other registered instances follow it, except the instance being uprooted. This lets a teardown distinguish another instance that still needs a shared resource from the source alone. Pending planting entries are included when they are siblings and excluded when they are the target. If the target's registry entry is missing, grove uproot refuses before teardown; if a pending target has no directory or no teardown script, teardown is not dispatched. In either case no process receives GROVE_SIBLINGS_JSON.

grove plant prints a --- grove-output --- JSON block for callers that need the created path, slot, ports, state ref, the branch and commit each repo landed on, and the recorded spec and applied intent.

grove plant, grove uproot, grove apply, grove restore, and grove release mark their in-progress instance planting, uprooting, applying, restoring, or releasing before long work and release the registry lock while that work runs. grove list, grove doctor, and grove ui show the state and its resolving command. grove dev, lifecycle commands, grove snapshot, and other conflicting operations refuse an in-progress instance. Remove a partial planting with grove uproot <project/name> --force; re-run grove uproot, grove apply, grove restore <project/name> <ref>, or grove release <project/name> to finish an interrupted operation of that type. If release reports repository changes, re-run it with grove release <project/name> --force. grove uproot refuses an instance being applied or restored, but remains the cleanup path for planting and uprooting instances. Once an operation succeeds, Grove clears its marker. An instance whose state was not applied is still marked state not applied and can be repaired with grove restore.

Grove does not infer a moved config path. Re-run grove register <source> --config <relative-path> --update; config-backed re-registration replaces stored ports, aliases, init, teardown, and development-command values.