Skip to content

grove CLI

Terminal window
npm i -D @groveback/cli
npx grove --help

Plain Node, zero runtime dependencies. Two jobs: generating a typed client, and local development against a project’s real shape.

  • The admin key comes from $GROVE_ADMIN_KEY or --key. It is never written to grove.json or the generated client.
  • --target remote|local (default remote) on gen, pull, push, seed and status. local talks to the grove dev server recorded in .grove/dev.json and 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.

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.

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.

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.

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.

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.

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.

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

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.

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.

<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, integernumber, arrayArray<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:

Terminal window
GROVE_URL=http://localhost:8099 npm run dev

Collections without an object schema fall back to the plain SDK surface. Custom /run/* endpoints are not emitted yet.