railcode
v0.2.6
Published
Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps.
Downloads
1,577
Readme
railcode CLI
Developer CLI for the multi-tenant Railcode platform: log in, scaffold, and deploy static apps from the terminal. It authenticates with a personal API token (long-lived, revocable) and uses the existing multipart deploy API.
Install
pnpm install
pnpm build # → dist/index.js (the `railcode` bin)Then run node dist/index.js <command>, or pnpm link --global to put railcode
on your PATH.
Commands
railcode login [--api-url <url>] Sign in and mint a personal API token
railcode login --setup-token <token> Sign in with a one-time onboarding setup token
railcode init <app> [dir] [--template react|static] [--force]
Scaffold an app. dir defaults to ./<app>;
pass "." to scaffold into the current dir
railcode deploy [--private] [--no-source] [--force]
Build (if configured) and deploy the app here
railcode pull [<deploy>] [--app <slug>] [--dir <path>] [--force]
Download a deploy's source (default: the live one)
railcode db <list|query> ... List data connectors / run read-only SQL
railcode connections <list|create|delete> ... Manage org data connectors (admin)
railcode connector <list|docs|fetch|native|enable|create|delete> ...
Call service connectors / manage them (admin)
railcode agent <list|show|create|update|delete|run|schedule> ...
Manage org-scoped managed agents
railcode members <list|set-role|remove|add> ... Members + system roles (admin)
railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|revoke|materialize|effective|catalog> ...
Org roles + the granular grants table (admin)
railcode apps <list|show|access|set-access|add-editor|remove-editor|add-viewer|remove-viewer|transfer|archive|unarchive|delete> ...
Apps + access policy (owner/admin; editors for
access/add-viewer/remove-viewer)
railcode app kv <collections|list|get|set|delete|drop> ...
Manage an app's KV store (owner)
railcode app files <list|download|upload|delete> ...
Manage an app's files (owner)
railcode llm <providers|models> The org's callable models, as apps see them
railcode llm connect chatgpt Bill LLM calls to a ChatGPT plan (admin)
Signs in with a login of its own; --from-codex
reuses ~/.codex/auth.json (shared token, see help)
railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics
railcode logs <connector|service-connector|llm|email|agent> [filters]
Org observability logs (admin)The members, roles, apps, analytics, logs, connections, and the
connector admin subcommands mirror the web console's admin surfaces — everything
an owner/admin can do in the dashboard is available from the CLI. They all target
your saved org and are gated server-side by the same capabilities as the console
(a plain member gets 403; app list/show/access follow the per-app access policy).
loginopens your browser against the server URL (defaulthttps://api.railcode.app) to authorize the CLI, then mints a personal API token, resolves your organization, and saves everything to~/.railcode/config.json(dir0700, file0600). A short-lived localhost callback catches the code automatically: the authorize page probes it and only navigates there once the CLI answers. When it can't be reached — browser on another machine, or one that won't follow a link to127.0.0.1— the Railcode page shows the code instead (with a copy button); paste it into the waiting prompt. Pasting the.../railcode-cli/callback?state=...&code=...address works too, if a browser does end up on an unreachable page: the CLI reads the code out of it, checking thestatematches this login run.--paste/--no-browserskips the callback entirely and goes straight to the code page — the right default for SSH/headless boxes.login --setup-token <token>is the non-interactive onboarding path (no TTY or browser — used by the dashboard's copied setup prompt). A setup token (rc_setup_...) is a short-lived (~10 min), ONE-TIME bootstrap credential, not a personal API token: the CLI exchanges it once for a personal API token and writes the same config as a browser login. If it's expired or already used, generate a fresh prompt from the dashboard.init <app> [dir]scaffolds a directory.diris optional and defaults to./<app>; pass.to scaffold into the current directory, or any path to scaffold into it. The target need not be empty (same-named files are overwritten), but an existingrailcode.jsonis preserved unless you pass--force. By default it creates a React + Vite + Zustand starter — a personalized "Welcome to Railcode,<name>" home wired to every SDK capability: identity/team, a live to-dos list and file uploads (work with zero setup), and SQL, saved queries, service connectors, and LLM cards that call the real thing when configured and fall back to a labeled sample otherwise. Hovering a name or card title reveals the exact SDK call behind it; every write fires a toast with the call that just ran. Use--template staticfor a no-buildindex.html(loads/_api/sdk.jsand demosawait me()+db.collection().put/get) plusrailcode.jsonwith"dist": ".".deployreadsrailcode.json({ app, build?, dist? }), runs the resolved build command when needed, then uploads the output dir by app slug. On the first successful deploy for a slug, the server creates the app as part of that deploy; a failed first deploy does not leave a visible not-deployed app behind. The live URL is printed after success. It also uploads the project source alongside the built files, and sends the deploy number this folder is based on — see Source,pull, and the version marker.--no-sourceskips the source tree;--forcedeploys over a base someone else has moved past;--privatesets access to private for this deploy only.pull [<deploy>]downloads the source stored with a deploy — the live one by default, or the number the dashboard's deploy history shows (#3— numbers count from 1 per app).--app <slug>picks the app when there's norailcode.jsonhere,--dir <path>writes somewhere other than the current folder, and--forceoverwrites local files that differ. Files the deploy doesn't contain are never deleted. This requires a server with deploy source history endpoints; older servers keep accepting deploys, butpullreports that source pull is not supported yet.appsmanages apps and their access policy. Grants come in three tiers: owner, editor, and member (viewer). An editor candeploy, read/revert deploy history,pullsource, read analytics, read the access policy, and add/remove individual viewers — but not delete, archive, transfer, change the access mode or the editor list, or useapp kv/app files(those stay owner/admin). Editors can open the app in every access mode and keep the grant across mode changes, so the editor list is not an audience list;railcode apps accessgroups grants by tier to keep that clear. Use the atomicadd-editor/remove-editor(andadd-viewer/remove-viewer, restricted mode only) to change one person without rewriting the whole policy; onset-access, omitting--editorsleaves the editor list alone while--editors ""clears it.transferdemotes the previous owner to editor rather than cutting them off.ci githubwires the app up to deploy from GitHub Actions, in one command. It resolves the app fromrailcode.jsonand the repository from theoriginremote, mints a deploy token, hands it to GitHub as the repository secretRAILCODE_API_TOKENvia theghCLI (over stdin — the plaintext never reaches your screen, your shell history, or an argv), and writes.github/workflows/railcode-deploy.yml. Withoutgh, or with--no-secret, it prints the token once plus the exactgh secret setcommand.--repo owner/nameand--branch <name>override what it detects;--forceoverwrites an existing workflow file. Nothing is added torailcode.json.tokenmanages those deploy tokens directly:create(plaintext shown once;--name <label>,--expires-in-days <n>),list,revoke <prefix>. The app comes from--apporrailcode.json. A deploy token can deploy that one app and nothing else — it cannot read data, list apps, revert a deploy, or mint another token. It is EDIT-tier to create, every owner/editor/admin of the app can revoke any of them, and it stops working if whoever created it loses edit rights on the app. Long-lived by default: an expiring CI credential breaks a pipeline with no warning, so a lifetime is opt-in.dbinspects the org's data connectors (per-org Postgres) and runs ad-hoc read-only SQL. It works straight afterrailcode login— no app and norailcode.jsonrequired — since connectors are org-scoped; it hits the app-less/api/organizations/{org}/data/*plane with your login token.railcode db list(aliasesls,connections) prints each connector'sname+engine(--jsonfor the raw array).railcode db query "<sql>"(aliassql) runs SQL against--connection(defaultdefault) and prints a table + row count;--engine <postgres|bigquery|turso>is inferred from the connector list when omitted,--params '<json-array>'binds$1, $2, …placeholders (SQL is never interpolated),--file <path>reads SQL from a file, and--jsonprints the raw{ columns, rows, rowcount, truncated }envelope.agentmanages org-scoped managed agents when your permissions allow it. Manifests are JSON or YAML files in the same shape the API stores underagent.manifest.railcode agent listlists agents;--jsonprints rawAgentOut[].railcode agent show <name|uuid>prints one agent;--manifestprints only its manifest JSON.railcode agent pull <name|uuid> --output agent.jsonwrites the existing manifest for editing.railcode agent create --file agent.yamlcreates an agent;update <agent> --file agent.yamlreplaces an existing agent manifest;delete <agent> --yesarchives it.--visibility <org|personal>oncreate/update/testsets or changes who the agent belongs to (defaultorgon create; omitted onupdateleaves it alone). Apersonalagent is invokable and manageable by its creator alone — no grant makes it shared — andpersonal -> orgis blocked once created. Requiresagent:create(personal) oragent:create_org(org).agent show/listprintvisibility, andshowprintsOwner:for a personal agent.railcode agent test --file agent.yaml --input '{"k":"v"}'runs a draft manifest without saving;railcode agent run <agent> --input '{"k":"v"}'invokes a saved agent and prints the result. Use--tracefor the step trace and--jsonfor the raw run detail.railcode agent schedule set <agent> --cron "0 9 * * *" --timezone UTCcreates or updates the agent's one schedule.schedule show,pause,resume,delete --yes, andrun-nowoperate on that single schedule.
personal-connectors(aliasespersonal-connector,pc) manages your own connected third-party accounts (Gmail, Slack, …), brokered by railcode (in-house) — distinct from org-admin service connectors and from an agent'stools.personal_connectors.listshows toolkits this deployment brokers and your connection status;tools <toolkit>prints that toolkit's callable tools and input schemas;connect <toolkit>prints an OAuth URL to open in a browser;call <toolkit> <tool> [--args '<json>']runs one tool on your own connected account. Hits the non-org-scoped/api/personal-connections/*plane — a personal connection belongs to the human, not the org.
Configuration
~/.railcode/config.json:
{ "apiUrl": "...", "email": "...", "apiToken": "rc_...", "tokenPrefix": "rc_...", "orgUuid": "...", "orgSlug": "..." }API URL resolution: --api-url > RAILCODE_API_URL > saved config > prompt.
Set RAILCODE_API_TOKEN to override the saved token in CI. On a 401, the saved
token is cleared and you're prompted to railcode login again.
In CI there is no saved config at all, so the org uuid has to come from the
environment too: set RAILCODE_API_TOKEN, RAILCODE_API_URL and
RAILCODE_ORG_UUID. Only the first is a secret; the other two are identifiers.
railcode ci github writes all three for you.
Staying up to date
The CLI keeps itself current. When you run a command in an interactive terminal it
checks the npm registry — at most once every 6 hours — for a newer railcode
release within your current major and, if there is one, installs it globally
(with whichever package manager it detects: npm/pnpm/yarn/bun) and tells you:
railcode: a new version is available — 0.1.26 → 0.1.27. Updating…
railcode: updated to 0.1.27. It takes effect on your next command.It never jumps across a major version (a breaking 2.0.0 is left for you to
install deliberately), the check is best-effort (a registry/network hiccup is
silently ignored — it never blocks your command), and it's skipped in
non-interactive/CI runs so automation never self-mutates a global install. The
last_checked_for_updates timestamp lives in ~/.railcode/update-check.json, and
all of the above is printed to stderr so --json output stays clean.
Environment overrides:
RAILCODE_NO_UPDATE=1— turn auto-update off entirely.RAILCODE_UPDATE_DRY_RUN=1— check and print the command it would run, without installing (also forces the check even off-TTY; handy for scripts and testing).RAILCODE_REGISTRY_URL(falls back tonpm_config_registry) — point the check at a different npm-compatible registry.
railcode.json
{
"app": "dashboard",
"build": "npm run build",
"dist": "dist"
}build and dist are optional. Set "dist": "." for no-build static apps.
Deploy output resolution:
railcode.jsondistwins.- Otherwise
railcode.jsonbuildruns and uploadsdist/. - Otherwise a
package.jsonwith abuildscript runs<pm> run buildand uploadsdist/, where<pm>is the project's package manager — detected from apackageManagerfield or lockfile (pnpm/yarn/bun), defaulting tonpmwhen there's no lockfile. - Otherwise a root
index.htmlcan be deployed interactively; for CI set"dist": ".".
Source, pull, and the version marker
A deploy uploads two things: the built files that get served, and the
project source so it can be pulled back later. The source tree is the project
folder minus anything its .gitignore excludes, minus a built-in exclude list,
minus the resolved build-output directory. The built-in list (matched by exact
name at any depth) covers vendored deps and build output (node_modules,
.venv, venv, dist, build), VCS internals (.git, .hg, .svn),
framework/tool caches (.vite, .next, .nuxt, .svelte-kit, .astro,
.turbo, .parcel-cache, .cache, .output, .wrangler, .vercel,
.netlify, __pycache__, .pytest_cache, .mypy_cache, .ruff_cache,
.tox, .eggs, .ipynb_checkpoints, coverage, .nyc_output), agent/local
tooling state (.gstack, .playwright-mcp), editor and OS debris (.vscode,
.idea, .DS_Store, Thumbs.db, desktop.ini), env files (.env,
.env.local, .env.development.local, .env.production.local — .env.example
still ships), package-manager debug logs, and the .railcode marker. If that
tree is over the upload limit the deploy stops with a message naming the limit —
add what doesn't belong to .gitignore, or pass --no-source.
On servers without deploy source history support, the extra deploy fields are
ignored for compatibility: deploy still works, but no marker is written and
railcode pull reports that the server does not support source pull yet.
railcode pull brings a deploy's source back down. With no argument it pulls
whatever is live; railcode pull 412 pulls that deploy. It writes into the current
folder (or --dir <path>), never deletes files the deploy doesn't contain, and
stops rather than overwriting local files that differ unless you pass --force.
Both commands leave a version marker behind — .railcode, one small JSON file
recording the instance, org, app and deploy number this folder now matches:
{ "api_url": "https://api.railcode.app", "org_uuid": "…", "app": "demo",
"app_uuid": "…", "deploy": 412 }The next deploy from that folder sends the number as its base version, and the
server rejects the deploy (409) if the app has moved past it — so two people
working from the same pull can't silently erase each other. The rejection says who
moved it; resolve it with railcode pull && railcode deploy, or go over it on
purpose with railcode deploy --force.
Because the marker is written after every successful deploy — including the
first deploy of a brand-new app — plain edit-deploy iteration stays protected
without pulling. It is scoped to one app: a copied project folder, or one
re-pointed at another app or instance, sends no base version rather than the wrong
one. Keep it out of git (railcode init gitignores it for you): it is sync state
about your folder, so a colleague's git clone must not inherit it.
