grove CLI
npm i -D @groveback/clinpx grove --helpPlain Node, zero runtime dependencies. Two jobs: generating a typed client, and local development against a project’s real shape.
Conventions
Section titled “Conventions”- The admin key comes from
$GROVE_ADMIN_KEYor--key. It is never written togrove.jsonor the generated client. --target remote|local(defaultremote) ongen,pull,push,seedandstatus.localtalks to thegrove devserver recorded in.grove/dev.jsonand uses that server’s key — the environment variable is ignored, because it belongs to the remote project.- Flags mean no prompts. Every command runs non-interactively when all required parameters are passed, which is what makes it CI-safe.
- Errors print as
grove: <message>and exit 1.
grove init
Section titled “grove init”Links the app to a project by writing grove.json.
| Flag | Required | Default | Meaning |
|---|---|---|---|
--url |
yes | — | Server base URL, without the /api/v1 suffix. |
--project |
yes | — | Project id (proj_…). |
--out |
no | src/grove |
Where the generated client goes. |
grove gen
Section titled “grove gen”Reads the project’s live structure and writes <outDir>/grove.ts.
| Flag | Default |
|---|---|
--key |
$GROVE_ADMIN_KEY |
--target |
remote |
The URL baked into the file is always grove.json’s, never the --target you generated
against — baking localhost:8099 into a committed file would break the app for everyone else.
Without a grove.json (local-first), the local server’s URL is baked as the honest default.
It prints which collections came out typed and which did not; an untyped one lacks an object schema.
grove pull
Section titled “grove pull”Mirrors the project’s shape into groveback/.
| Flag | Default | Meaning |
|---|---|---|
--key |
$GROVE_ADMIN_KEY |
|
--force |
false | Overwrite even with uncommitted bundle changes. |
--target |
remote |
Pull replaces: a resource deleted upstream is deleted locally. It refuses to run over a
bundle with uncommitted changes (checked via git status --porcelain; outside a git repo the
check does not apply). Each pull records a sha256 baseline per file in .grove/pull.json.
grove push
Section titled “grove push”Applies groveback/ to the project as a merge-upsert. Always plans first, then prompts.
| Flag | Default | Meaning |
|---|---|---|
--key |
$GROVE_ADMIN_KEY |
|
--dry-run |
false | Show the plan and stop. Exit code 2 when there are changes, so CI can gate without parsing output. |
--yes |
false | Skip the confirmation. |
--verbose |
false | Include unchanged resources in the plan. |
--force |
false | Push even though the project moved since the last pull. |
--prune |
false | After applying, delete resources the bundle no longer mentions. |
--target |
remote |
Never deletes by default — a resource that exists only on the server is left alone. A non-TTY stdin answers “no” to the prompt: silence is never consent. On a mid-apply failure it advises re-running, since the import is idempotent though not transactional.
--prune deletes only functions, webhooks, integrations, roles and end-user roles.
Collections and buckets are never pruned — they hold data; orphans are reported with
advice instead. Vector configs and OAuth providers are excluded too, since they attach to a
collection or a provider slot. Every name is printed before anything is deleted, and confirmed
separately.
grove dev
Section titled “grove dev”Starts a local Groveback and applies the bundle to it.
| Flag | Default | Meaning |
|---|---|---|
--port |
8099 |
|
--project |
proj_dev |
Local project id. |
--mongo |
— | A MongoDB URL, to opt out of in-memory storage (real indexes, vector search). |
--seed |
false | Load seeds/ once the server is up. |
--persist |
false | On exit, write the server back: shape → bundle, documents → seeds/. |
--key |
$GROVE_ADMIN_KEY |
Only for the dashboard’s local-vs-project diff. |
Use --seed --persist together — see the local loop.
It locates the binary at node_modules/.bin/groveback (app-local only, a pinned dev
dependency — not PATH), mints the admin key before the server exists, refuses to start if
something already answers on the port, and spawns detached in its own process group so a
terminal Ctrl-C reaches only the CLI. That last detail is what makes --persist possible: the
snapshot must read state out of a still-answering server.
grove seed
Section titled “grove seed”| Flag | Default | Meaning |
|---|---|---|
--collection |
all | Only this collection. |
--reset |
false | Delete existing documents first. Local servers only. |
--dump |
false | Write a local project’s documents out as fixtures instead. |
--key |
$GROVE_ADMIN_KEY |
|
--target |
remote |
--reset is refused unless the target is the grove dev server or a loopback host. --dump
is local-only by construction — pulling documents out of a remote project is deliberately not
built.
Fixtures live in seeds/<collection>.<json|ndjson|jsonl>. The format is sniffed from the
first non-whitespace character: a leading [ means a JSON array, anything else is NDJSON.
Errors report the line number or array index. Files are read in sorted order, so seeding is
deterministic.
--dump writes a pretty-printed array and strips createdAt, updatedAt and ownerId.
id is kept — it is accepted on create, which makes a seed run idempotent and keeps
cross-references between fixtures stable.
grove status
Section titled “grove status”Read-only. Prints a dry-run plan of what a push would change, plus whether the project moved since the last pull.
| Flag | Default |
|---|---|
--key |
$GROVE_ADMIN_KEY |
--verbose |
false |
--target |
remote |
grove.json
Section titled “grove.json”Non-secret by design — commit it.
{ "url": "https://api.example.com", "project": "proj_abc123", "outDir": "src/grove", "bundleDir": "groveback", "seedDir": "seeds"}| Field | Default | Meaning |
|---|---|---|
url |
required | Base URL, no /api/v1. Trailing slashes stripped. |
project |
required | Project id; must match the admin key’s project. |
outDir |
required | Where grove gen writes. |
bundleDir |
groveback |
The committed config bundle. |
seedDir |
seeds |
Data fixtures. |
grove dev deliberately does not require a grove.json — a local-first app has no
deployed project yet, so only bundleDir, seedDir and outDir are read.
.grove/ local state
Section titled “.grove/ local state”Gitignored, and self-ignoring: it writes its own .gitignore containing *, so a missing
app-level .gitignore can never leak the dev key.
| File | Contents |
|---|---|
pull.json |
{ at, url, project, files: { path: sha256 } } — the divergence baseline. Ignored if taken against a different project or URL, so promoting a dev bundle into a fresh project is not blocked as a clobber. |
dev.json |
{ url, project, adminKey, pid, startedAt } — the only key on disk, and only an in-memory local server’s. |
local-status.json |
The dashboard’s Local-section snapshot. |
The generated client
Section titled “The generated client”<outDir>/grove.ts, riding on @groveback/sdk at runtime.
Per schema-bearing collection you get two interfaces: <Name> (declared fields plus the
system fields id, ownerId?, createdAt?, updatedAt?) and <Name>Input (declared fields
only — the server stamps the rest).
Type mapping: enum → a literal union, integer → number, array → Array<T>, an object
without properties → Record<string, unknown>, unknown → unknown. description and x-ref
become one-line JSDoc.
export interface TypedCollection<TDoc, TInput> { find(opts?: FindOptions): Promise<TDoc[]>; get(id: string): Promise<TDoc>; create(doc: TInput): Promise<TDoc>; update(id: string, patch: Partial<TInput> & Doc): Promise<void>; replace(id: string, doc: TInput): Promise<void>; delete(id: string): Promise<void>; search(opts: { text?: string; vector?: number[]; limit?: number; filter?: Doc }): Promise<Array<TDoc & { _score: number }>>; subscribe(event: ChangeType | '*', cb: (event: ChangeEvent) => void): Unsubscribe;}
export const GROVE_URL: string;export function createGrove(options?: GroveOptions): { client; // the raw SDK client — the escape hatch auth; storage; graphql; collections: { [name]: TypedCollection<Doc, Input> };};Usage:
import { createGrove } from './src/grove/grove';
const grove = createGrove();await grove.auth.login('a@example.com', 'password');const posts = await grove.collections.posts.find();await grove.collections.posts.create({ title: 'Hello' });URL resolution, most specific first: an explicit baseUrl option → the GROVE_URL
environment variable → the baked GROVE_URL constant. That is what lets one committed
client serve both grove dev and production:
GROVE_URL=http://localhost:8099 npm run devCollections without an object schema fall back to the plain SDK surface. Custom /run/*
endpoints are not emitted yet.