@groundfloorcloud/cli
v0.2.7
Published
Groundfloor gf CLI — sign in, deploy coderunners, publish Shell apps, and manage secrets, files, and Dataplane.
Readme
@groundfloorcloud/cli (gf)
Sign in to Groundfloor, manage workspaces, and work with Coderunner, secrets, feature flags, files, apps, and Dataplane from your terminal.
gf login opens a browser, you sign in, and the session is cached and
refreshed automatically.
Agents in Cursor use @groundfloorcloud/mcp against the
same gf login session to inspect the workspace and docs, then run gf
commands to take action.
Install
npm i -g @groundfloorcloud/cli
gf loginWithout a global install:
npx @groundfloorcloud/cli loginRequires Node.js 20 or later. gf checks npm at most once a day and prints a
one-line upgrade notice when a newer @groundfloorcloud/cli exists. Unknown
subcommands include the installed vs latest version so a stale binary is not
read as wrong docs.
gf login signs in to production
(https://platform.groundfloor.cloud). Use --dev or --stage for
other cells; later commands reuse that session.
Quick start
gf login # production
gf login --stage
gf login --dev
gf whoami
gf workspaces
gf workspaces use <uuid>
gf coderunner ls
gf deployNon-interactive use (CI, containers): set GROUNDFLOOR_TOKEN to a Control
Plane JWT. It takes precedence over the saved gf login session (an expired
GROUNDFLOOR_TOKEN is ignored when a saved session exists, so a stale export
does not shadow it). Mint one from an M2M client:
export GROUNDFLOOR_TOKEN="$(gf m2m token)" # needs GROUNDFLOOR_M2M_CLIENT_ID / _SECRET
export GROUNDFLOOR_WORKSPACE_ID=<workspace-id>
# non-production cells also need GROUNDFLOOR_API_URL
gf coderunner lsUse the session from a shell or script:
eval "$(gf env)"
curl -H "Authorization: Bearer $(gf token)" \
"$CONTROLPLANE_URL/v1/workspaces"Deploy workloads
gf deploy packages your code and runs the full Coderunner lifecycle:
create if needed, upload a version, wait for the build, deploy, poll until
complete, and print IDs and the URL.
Do not create an App just to run a function, job, schedule, or service
(Deployment). Use gf deploy. Bind --app-id only when the coderunner should
be a helper of an existing product App.
App authentication is optional. Team write can create Coderunners
when mode is None. For Vault and Files from the workload, attach an M2M
client (Access → Machine clients) and use gf m2m token — not a Dataplane
key. gf coderunner vault-token is deprecated.
Workload types
| --workload-type | Also called | Package notes |
|-------------------|-------------|---------------|
| function | Function | HTTP on 0.0.0.0:PORT/8080. Node needs scripts.start. |
| job | Background job | Run-to-completion. Then gf coderunner run. |
| schedule | Scheduled job | Cron via --schedule or groundfloor.json schedule. No public URL. |
| service | Deployment | Long-running HTTP. Root Dockerfile is required. |
gf deploy
gf deploy --git https://github.com/me/my-fn.git --ref main
gf deploy --name my-fn --runtime python --cpu 250m --memory 256Mi \
-e LOG_LEVEL=debug
gf deploy --workload-type service
gf deploy --workload-type schedule --schedule "*/5 * * * *"
gf deploy --app-id <app-uuid>How the target is chosen:
--coderunner <id>deploys into that coderunner.- Otherwise the name comes from
--name, thengroundfloor.json, then the folder or repo name. A matching slug is reused; otherwise a new coderunner is created. --app-idapplies only when a new coderunner is created. When an existing one is reused or-cis given, it is ignored andgf deploywarns.
Deploy target safety:
- The workspace comes from
-w,GROUNDFLOOR_WORKSPACE_ID, or the saved machine-wide default (gf workspaces use). The saved default is shared by every shell on the machine, so it is never used silently: without an interactive terminalgf deployrefuses and asks for-w <workspace-id>; on a terminal it prints the workspace name and id and asks to confirm.-wandGROUNDFLOOR_WORKSPACE_IDdeploy without that prompt. - Reusing a same-named coderunner prints its name, id and environment and asks
to confirm. Without a terminal, pass
-y/--yes. - Check the printed coderunner id and URL after the deploy; an unexpected new id means it went somewhere you did not intend.
Redeploys and env vars. The platform stores only the env var names of a
deployment, never the values. A redeploy that does not resend a variable
(-e, or groundfloor.json env) drops it from the new deployment. gf
deploy warns about previously-set keys that are not being resent. Resend every
variable you need on each deploy.
Secrets on deploy. Never pass credentials with -e (credential-shaped
names are rejected). Store the secret in the coderunner's environment and
deploy with a reference:
printf '%s' "$PF_SECRET" | gf secrets set PF_SECRET - -e <environment-id>
gf deploy --secret PF_SECRET # env var PF_SECRET from secret PF_SECRET
gf deploy --secret DB_PASS=prod_db_password # env var DB_PASS from secret prod_db_password
gf deploy --no-secrets # remove every saved reference- Only names travel: the Control Plane reads the values from the coderunner's
bound environment (no fallback to
dev) and hands them to the workload through a Kubernetes Secret. Values never appear in the deploy body, the CLI output, logs or API responses. - References are saved. A deploy without
--secretkeeps the last deploy's references (andgf deploysays which); any--secretreplaces the whole list;--no-secretsremoves them. References come from flags only, not fromgroundfloor.json. A changed secret value reaches the workload on the next deploy. - Secret references must be switched on for the Groundfloor cell. Until they
are, a deploy with
--secretis refused with422, reasonsecret_refs_disabled; read the secret at runtime instead with an M2M client that hassecrets:read(gf m2m token, thenGET /v1/workspaces/{id}/secrets/{key}?environment_id=…). - Refusals print the server message, the
reason:code and one hint for that code:secret_ref_missing(not in the coderunner's environment),secret_ref_unbound(no active environment),secret_ref_invalid,secret_ref_forbidden(platform-managed secret, or an M2M caller withoutsecrets:read),secret_ref_too_large(64 references / 64 KiB),secret_ref_version_ineligible,secret_ref_store_failed(502) andsecret_ref_apply_failed(502/503; nothing was deployed).
Runtime is auto-detected when omitted (package.json → node,
requirements.txt / pyproject.toml → python), defaulting to python.
Node without a Dockerfile uses the platform bootstrap (CMD ["npm","start"],
port 8080). Add "scripts": { "start": "node index.js" } and bind
0.0.0.0. gf deploy fails immediately if scripts.start is missing.
Local with live data
eval "$(gf env)"
export PORT=8080
npm start
# files/secrets / operator Vault (builder token):
curl -sS -H "Authorization: Bearer $(gf token)" \
"$CONTROLPLANE_URL/v1/workspaces/$GROUNDFLOOR_WORKSPACE_ID/files"
# M2M (Coderunner / CI — not gf token):
curl -sS -H "Authorization: Bearer $(gf m2m token)" \
"$CONTROLPLANE_URL/v1/workspaces/$GROUNDFLOOR_WORKSPACE_ID/vault/collections"Then gf deploy the same tree. Do not zip .env files. gf token is the
wrong identity for testing Coderunner Vault/Files as the workload.
groundfloor.json
{
"name": "my-fn",
"runtime": "python",
"workloadType": "function",
"cpu": "250m",
"memory": "256Mi",
"env": { "STAGE": "dev" },
"schedule": "*/5 * * * *"
}Deploy flags
| Flag | Description |
|------|-------------|
| -w, --workspace <id> | Target workspace. Required without a terminal unless GROUNDFLOOR_WORKSPACE_ID is set; the saved default needs an interactive confirm. |
| -n, --name <name> | Coderunner name (else groundfloor.json, then folder name). |
| -c, --coderunner <id\|slug> | Deploy into this existing coderunner. |
| --app-id <uuid> | Bind a new coderunner to an App as a helper. Ignored on reuse. |
| --secret <NAME\|ENV=SECRET> | Reference a secret from the coderunner's environment (repeatable). Any --secret replaces the saved references; omitted keeps them. Needs secret references switched on for the cell. |
| --no-secrets | Remove every saved secret reference. |
| --api-url <url> | Control Plane base URL override. |
| -y, --yes | Skip the reuse confirmation (required without a terminal) and the prompt shown with -c when the deploy would drop previously deployed env variables (key names only; values are not stored). |
| -p, --path <dir> | Directory to package (default: cwd). |
| --git <url> / --ref <ref> | Clone and deploy a remote repo. |
| --subdir <dir> | Package a subdirectory of the source. |
| --runtime <r> | Language slug from GET …/coderunners/languages (node, python, go, dotnet, dotnet-script, …). |
| --workload-type <t> | function | service (Deployment) | job | schedule. Service requires a Dockerfile. |
| --schedule <cron> | Cron for schedule workloads (or groundfloor.json schedule). |
| --cpu / --memory | Resource requests, e.g. 250m / 256Mi. |
| -e, --env KEY=VALUE | Deployment env var (repeatable). |
| -m, --message <text> | Version description. |
| --skip-deploy | Upload a new version but do not deploy it. |
What is never uploaded
gf deploy drops credential files from the package on both paths — the
plain-directory walk and the git file list (which includes untracked files that
no .gitignore rule covers). Excluded by name: .env*, .npmrc, .yarnrc*,
.netrc/_netrc, .pypirc, .git-credentials, .htpasswd, .pgpass,
.s3cfg, id_rsa/id_dsa/id_ecdsa/id_ed25519 (but not their .pub
halves), *.pem, *.key, *.p12, *.pfx, *.jks, *.keystore, *.ppk,
*.kdbx, *.gpg, *.tfstate*, and anything under .ssh/, .aws/, .azure/,
.gcloud/, .gnupg/, .docker/, .kube/, .terraform/ or .chef/.
Every exclusion is printed, and so is every dot-file that survived the filter.
There is no flag to turn it off: keep credentials in gf secrets and reference
them with --secret or read them at runtime (M2M secrets:read), or pass non-sensitive config with
-e KEY=VALUE. Credential-shaped -e keys are rejected.
This is a denylist, so it only knows names. A secret in a file called
config.json, settings.py or serviceAccount.json is still uploaded —
check what the packaged file list says before deploying.
Publish a Shell app
Product Apps are packaging (Shell Module Federation remotes, or a
standalone wrapper around a service). That is separate from gf deploy.
Always start from the official starter-kit. Do not scaffold App.tsx or
put BrowserRouter in the federated export — that crashes the Shell.
gf apps init --slug my-federated-app
cd my-federated-app
gf apps create --name "My Federated App" --slug my-federated-app \
--manifest ./groundfloor.manifest.json
npm install
npm run release
gf apps publish --path release.zipgf apps init downloads the public ZIP from the Client Portal
(/downloads/shell-starter-kit.zip on the current cell) and stamps
APP_ID / groundfloor.manifest.json appId to the slug.
gf apps publish uploads only release.zip from npm run release.
It flattens a Vite dist/assets/ layout when needed, rewrites ./assets/
imports in remoteEntry.js, then rejects a zip that is only
remoteEntry.js (missing __federation_*.js / CSS) or whose federation
name does not match federationRemoteName(slug). A lone .js file is
also rejected. Pass an app id/slug, or set appId in
groundfloor.manifest.json next to the zip.
Wrap an existing service coderunner as a product:
gf apps create --kind standalone --name "My Product" --slug my-product \
--primary-coderunner <coderunner-id>Do not create app_kind=coderunner. Functions, jobs, schedules, and services
use gf deploy.
App sign-in (end users of a standalone app)
In a workspace that uses Groundfloor login, every app has its own public
sign-in client, app-<app_id>. The app signs users in with PKCE; its backend
checks that tokens were issued to that client (use
@groundfloorcloud/app-auth).
gf apps auth my-product # client id, issuer, redirect URIs, origins, sync status
gf apps auth add-origin my-product http://localhost:5173 # extra web origin (never a redirect URI)
gf apps auth add-callback my-product /login/done # extra callback path on the app's own hosts
gf apps auth remove-origin my-product http://localhost:5173
gf apps auth remove-callback my-product /login/doneRedirect URIs are exact: https://<host>/auth/callback (plus up to 5 extra
paths) on the app's public host and verified custom domains. Extra origins:
up to 20, https://host[:port] (a DNS name, not an IP address) or
http://localhost[:port] / http://127.0.0.1[:port], no wildcards. Extra
callback paths may not contain wildcards, queries, .. segments or encoded
., /, \ (%2e, %2f, %5c); gf checks this before sending. Viewing needs workspace read,
changing needs write. Workspaces that use their own identity provider have no
app client.
Secrets, files, Dataplane
gf dataplane status
gf dataplane provision
gf secrets ls
printf '%s' "$DATABASE_URL" | gf secrets set DATABASE_URL - # stdin keeps the value out of shell history; empty stdin is refused
gf secrets get DATABASE_URL
gf secrets rm DATABASE_URL
gf files ls
gf files upload ./report.pdf
gf files download <file-id> -o ./report.pdf
gf files rm <file-id>
gf apps ls
gf apps get <id|slug>Secret keys become environment variables: letters, digits and _ only,
starting with a letter or _, at most 256 characters (TELNYX_API_KEY, not
telnyx-api; anything else is a 422). DATAPLANE_SERVICE_API_KEY and
MANAGED_DB_* are platform-managed and can't be set or removed (403).
gf env and gf secrets get print raw secret values to stdout by design; do
not run them where output is logged. For gf m2m token, set
GROUNDFLOOR_M2M_CLIENT_SECRET instead of --client-secret (argv is visible
in ps and shell history).
Feature flags
Per-environment toggles and config values. -e takes an environment id or name
(dev, staging, prod); without it the workspace default (dev) is used.
gf flags ls -e prod
gf flags set new-checkout true -e dev # boolean (inferred)
gf flags set max-items 25 -e dev # number (inferred)
gf flags set banner '{"text":"Hi"}' --type json -e dev
gf flags get new-checkout -e prod
gf flags compare dev prod
gf flags promote dev prod --dry-run # then without --dry-run
gf flags rm new-checkout -e devKeys are lowercase letters, digits, -, _ and ., starting with a letter.
Apps read flags at runtime from GET /v1/workspaces/{id}/flags/runtime: Shell
apps with the end-user token, Coderunners with gf m2m token (read-only). See
https://docs.groundfloor.cloud/docs/configure/feature-flags.
Commands
| Command | Description |
|---------|-------------|
| gf login [--dev\|--stage] [--device] | Browser sign-in (production default). |
| gf logout | Clear the cached session. |
| gf whoami | Show identity and verify the Control Plane session. |
| gf token | Print a fresh access token. |
| gf env | Print export … lines for eval "$(gf env)". |
| gf environments ls (envs) | List workspace environments. |
| gf workspaces (ws) | List workspaces; gf workspaces use <id> sets the default. |
| gf coderunner ls (cr ls) | List coderunners. |
| gf coderunner status -c <id\|slug> | Status and current deployment. |
| gf coderunner logs -c <id\|slug> | Process Log (stdout/stderr). Also printed automatically on gf deploy failure. |
| gf coderunner cancel-deployment -c <id\|slug> | Destructive. Deletes the current (or --deployment-id) deployment; if it is the live one the service goes offline. Confirms on a terminal; requires -y/--yes without one. |
| gf coderunner run -c <id\|slug> [--payload '{}'] | Invoke a deployed function or job. |
| gf m2m token | Print a short Control Plane JWT from M2M client_id + secret. Not gf token. |
| gf coderunner vault-token | Deprecated. Use gf m2m token. |
| gf deploy | Package code and deploy as a coderunner (see Deploy flags). |
| gf domains ls\|add\|rm\|verify --app <id\|slug> | Custom domains for a standalone App. Domains belong to Apps; standalone coderunner domain routes are not supported. |
| gf identities ls\|mint --identity <id> | Workload identities (Vault principals); first mint returns refresh_secret once. |
| gf usage [--account <id>] [--since-days <n>] [--json] | Read-only account quota/usage and billing estimate. The account is the billing parent of the workspace, not the workspace id. |
| gf api <path> [-X <method>] [--body <json>] | Call a Control Plane path; {workspace_id} expands to the current workspace. |
| gf secrets ls\|get\|set\|rm | Workspace secrets. |
| gf flags ls\|get\|set\|rm\|compare\|promote | Feature flags per environment (-e <id\|name>). |
| gf files ls\|upload\|download\|rm | Workspace files. |
| gf apps init --slug <slug> | Download the official Shell starter-kit and stamp APP_ID. |
| gf apps ls\|get\|create\|publish | Product apps (Shell publish / standalone wrap). |
| gf apps auth <app> / add-origin / add-callback / remove-origin / remove-callback | An app's sign-in client: show it, or change its extra web origins and callback paths. |
| gf dataplane status\|provision | Dataplane setup for the workspace. |
Configuration
Settings resolve as: command flags > environment > ~/.groundfloor/config.json
Groundfloor production.
gf login with no cell flag always uses production, even if a previous
--dev / --stage session is still in config.json. Pass --dev, --stage,
or --env <production|stage|dev> to pick another cell. --api-url / --issuer
still override the hosts for that login.
| Setting | Flag | Env | Production default |
|---------|------|-----|--------------------|
| Control Plane URL | --api-url | GROUNDFLOOR_API_URL | https://platform.groundfloor.cloud |
| IdP issuer | --issuer | GROUNDFLOOR_ISSUER | Groundfloor managed IdP |
| OIDC client id | --client-id | GROUNDFLOOR_CLIENT_ID | groundfloor-cli |
| Default workspace | (via gf workspaces use) | GROUNDFLOOR_WORKSPACE_ID | (none) |
| Access token | (none) | GROUNDFLOOR_TOKEN | (none; wins over the saved session) |
State is stored under ~/.groundfloor/:
auth.json— cached tokens (chmod600).config.json— non-secret defaults (issuer, client id, api url, workspace); chmod600.
Redirect URI mismatch
gf login uses a loopback callback (http://127.0.0.1:<port>/callback). If
sign-in fails with an invalid redirect URI:
- Prefer
127.0.0.1(the CLI default). - If your identity provider only allows
localhost, rungf login --redirect-host localhost. - To pin a port:
gf login --redirect-port 8976.
