Skip to content
garage
Esc
navigateopen⌘Jpreview
On this page

JavaScript SDK reference

Repository lifecycle handles and typed clients exported by @thegarage/sdk.

@thegarage/sdk provides authenticated and anonymous repository handles plus typed low-level clients. Install it with:

npm install @thegarage/sdk

The package targets Node.js 24 and later. Its authenticated porcelain APIs can also run in browser and Worker-style runtimes when token, baseUrl, and optionally fetch are passed explicitly.

Lazy authenticated handle

garage(name, options?): Promise<AuthenticatedRepo>

Creates a lazy repository handle. Construction performs zero network I/O.

Parameter Type Description
name string Bare repository name in the API key’s active organization (name resolution)
options.token string grg_ API key; falls back to GARAGE_API_KEY in Node.js
options.defaultBranch string Branch requested if a later write provisions the repository; defaults to main
options.baseUrl string API origin; falls back to GARAGE_SERVER_URL, then production
options.fetch typeof fetch Optional fetch implementation
const repo = await garage('my-repo', {
  token,
  defaultBranch: 'trunk',
})

console.log(repo.defaultBranch) // null until server metadata is observed

Reads never create a repository or call repos.ensure, so a read-only flow continues to work with only repos:read. The first commit() provisions a missing repository. ensure() provisions without committing and shares its memo with the first commit.

Strict create and import

garage.create(name, options?): Promise<AuthenticatedRepo>

Always performs I/O. Creation is strict: an existing name throws RepoExistsError rather than opening the existing repository.

const created = await garage.create('new-repo', {
  token,
  defaultBranch: 'trunk',
})

const imported = await garage.create('imported-repo', {
  token,
  from: 'https://github.com/acme/project.git',
})

from routes to the GitHub import procedure. Only https://github.com remotes are accepted. Rejections throw ImportRemoteRejectedError with that allowlist in the SDK message.

Created and imported handles retain their complete RepoView, including the clone remote, effective permissions, size, source, and timestamps.

List repositories

garage.list(options?): Promise<RepoPage<AuthenticatedRepo>>

Returns { items, nextCursor }. Each item is an authenticated handle seeded with its complete cached RepoView.

Option Type Description
cursor string Cursor returned by a previous bounded call
limit number Total-result cap; defaults to 100
token string Explicit API key, or GARAGE_API_KEY in Node.js
baseUrl string Explicit API origin, GARAGE_SERVER_URL, or the production default
fetch typeof fetch Optional fetch implementation

The server page maximum is 100. A total limit over 100 auto-paginates with requests of min(100, remaining) and stops exactly at the requested cap. The default does not enumerate an entire organization implicitly.

const first = await garage.list({ token })
if (first.nextCursor) {
  const second = await garage.list({ token, cursor: first.nextCursor })
}

Repository handles

The shared handle is Repo<TView extends RepoView>. Public handles are split into AuthenticatedRepo and AnonymousRepo so anonymous-only view fields and authenticated-only mutations remain correctly typed.

Metadata and lifecycle

Property or method Result Behavior
name string Repository name
defaultBranch string | null Server-observed branch; null before a lazy handle obtains a view
remote string | null Clone URL after create, import, list, ensure, commit, or info
expiresAt string | null Anonymous expiration; null for authenticated repositories
info({ refresh? }) Promise<TView> Returns a copy of the cached view, or fetches with repos.get
exists() Promise<boolean> Always checks the correct authenticated or anonymous lifecycle lane
delete() Promise<void> Deletes through the correct lane and makes the handle terminal on success
ensure() Promise<RepoInfo> Provisions a lazy authenticated repo; no-op RPC-wise for known handles
setDefaultBranch(branch) Promise<void> Authenticated only; updates through repos.update

info() does not expose mutable cache state. Use info({ refresh: true }) to force a server read. exists() never provisions a repository.

An exists() that finds the repository gone clears the cached view and the memoized ensure() result together. The next ensure() re-runs create-or-open against the server instead of replaying the earlier outcome; anonymous handles cannot provision, so theirs throws RepoNotFoundError.

After successful deletion, exists() returns false; cached metadata is cleared and all other operations throw DeletedRepoHandleError. Construct a new handle to recreate or reattach. Anonymous deletion does not clear the persisted anonymous identity.

Git operations

Method Result Behavior
add(path, content) Promise<void> Stages a string or Uint8Array in memory
remove(path) Promise<void> Stages a deletion
commit(message, options?) Promise<Commit> Atomically writes all staged changes
read(path, ref?) Promise<Blob> Returns UTF-8 text, bytes, and the blob SHA
ls(ref?, options?) Promise<TreeEntry[]> Lists tree entries; recursive by default
log(options?) Promise<Commit[]> Lists commits; defaults to 50

For a missing lazy authenticated repository, ls() and log() return []. read() throws PathNotFoundError, matching an empty create-or-open handle. An existing repository with a missing path throws the same typed error. Anonymous 404s are never normalized.

Anonymous repository

garage.ephemeral(options?): Promise<AnonymousRepo>

With no name, creates a new randomly named repository. With name, first reattaches if the persisted identity owns that repository and creates it when missing. Concurrent creators converge by retrying the read when another creator wins the race. Supplying a TTL makes the named creation strict: if the repo already exists, TtlReattachUnsupportedError is thrown instead of silently ignoring the requested lifetime.

Option Type Description
name string Named repository to reattach or create
ttl string Lifetime such as 30m, 6h, or 2d
ttlSeconds number Lifetime in seconds
baseUrl string API origin
configDir string Node.js directory used to persist the anonymous identity securely
fetch typeof fetch Optional fetch implementation

Pass either ttl or ttlSeconds; supplying both throws TtlConflictError. The accepted range is 60 seconds through 72 hours. configDir treats a missing anon.json as a new identity, but a damaged file throws InvalidAnonIdFileError.

Authentication resolution

Authenticated entry points use this precedence:

  1. options.token, then Node.js GARAGE_API_KEY
  2. options.baseUrl, then Node.js GARAGE_SERVER_URL, then https://api.thegarage.sh

An explicitly empty token throws MissingTokenError instead of falling back. Environment lookup uses guarded globalThis.process?.env access, so runtimes without process can use explicit options without a ReferenceError.

Errors

Every validation or lifecycle failure defined by the SDK extends GarageError and has a stable code. Porcelain RPC failures extend GarageRpcError, preserving status, structured data, the server message, and the raw oRPC error as cause. Known subclasses include:

  • RepoNotFoundError, RepoExistsError, ForbiddenError
  • QuotaExceededError, PayloadTooLargeError
  • ImportRemoteRejectedError, ImportTooLargeError, ImportFailedError
  • MissingTokenError, InvalidRepoNameError, InvalidApiKeyError
  • InvalidTtlError, TtlConflictError, TtlReattachUnsupportedError, DeletedRepoHandleError
  • InvalidAnonIdFileError, ConfigDirPersistenceUnsupportedError

Native network/runtime failures remain native errors.

Low-level clients

createClient(options): GarageSdkClient
createAnonClient(options): GarageAnonSdkClient

These expose the typed raw oRPC contracts. They intentionally retain raw oRPC errors rather than the porcelain GarageRpcError mapping.

Examples

Was this page helpful?