Skip to content

Latest commit

 

History

History
427 lines (319 loc) · 20.7 KB

File metadata and controls

427 lines (319 loc) · 20.7 KB

@localess/cli Reference

Command-line interface for the Localess headless CMS. Handles authentication, translation sync, TypeScript type generation, and schema pull/diff/push.

Requires: Node.js >= 24.0.0.

Installation

npm install -g @localess/cli
# or without installing
npx @localess/cli [command]

Authentication

localess login

localess login \
  --origin https://my-localess.web.app \
  --space YOUR_SPACE_ID \
  --token YOUR_API_TOKEN
Flag Description
-o, --origin <url> Localess instance URL
-s, --space <id> Space ID (from Space settings)
-t, --token <token> API token (masked input)
-v, --verbose Print verbose debug output

What it does:

  1. Checks for existing credentials (env vars or file) — if found, prints Already logged in. and exits (run logout first to switch)
  2. Prompts interactively for any missing options
  3. Validates credentials against the API (GET /spaces/{spaceId}); exits 1 without saving on failure
  4. Saves credentials to .localess/credentials.json (mode 0o600)
  5. Appends .localess to .gitignore automatically (creates the file if absent)

localess logout

localess logout

Clears .localess/credentials.json (overwrites it with {}). If authenticated via environment variables, instructs you to unset them manually.

Credential Resolution

Credentials are resolved in this priority order:

  1. Environment variables (highest priority — recommended for CI/CD; all three must be set)
  2. .localess/credentials.json in the current working directory (file-based — for local development)

Every command that talks to the API accepts -v, --verbose to print client debug output (request URLs, statuses). Commands that need credentials exit 1 with Not logged in when neither source is available. After any command, the CLI checks the npm registry (3-second timeout, failures ignored) and prints an "Update available" box if a newer @localess/cli exists (pre-release installs also check the dev tag; a newer stable release wins).

Platform Compatibility Check

The CLI and the platform are released in lockstep: a 4.x.x CLI is designed against a 4.x.x platform. Before running a command, the CLI reads the platform's version from <origin>/assets/version.json and enforces two rules:

  1. The majors must match. A 4.x.x CLI refuses a 3.x.x or 5.x.x platform.
  2. The platform must meet the command's minimum, from the compatibility matrix below.

A confirmed incompatibility blocks the command — every command, not only the ones that write — and exits 1 before any API call, because an unmet requirement means the command cannot work correctly.

Compatibility matrix

Command Minimum platform
login — (no platform call)
logout — (no platform call)
schema validate — (reads local files only)
schema pull 4.0.0
schema diff 4.0.0
schema push 4.0.0
translation pull 4.0.0
translation push 4.0.0
translation diff 4.0.0
type generate 4.0.0

Entries are minimums, not ranges. A future platform that breaks a command can't be predicted from the CLI, so that direction is covered by rule 1 instead. Every command must appear in the matrix — a test walks the command tree and fails on a missing or stale entry, so adding a command forces a deliberate choice.

When the check does not block

Situation Behaviour
Both rules satisfied Runs normally, prints nothing
Command makes no platform call Skipped, so it works offline and against any platform
No credentials configured Skipped — the command reports the auth problem itself
Version undeterminable Skipped silently and the command proceeds
LOCALESS_SKIP_VERSION_CHECK set Skipped entirely; the version is not even fetched

"Undeterminable" covers a non-2xx response, an unreachable host, a malformed body, and a 3-second timeout. A network problem can never stop a command — only a confirmed incompatibility does.

That version file is emitted by the platform's Angular build and served by Firebase Hosting, so it is present on every normally deployed instance. It is not served by an API-only or emulator-only deployment, where the check reports "unknown" and proceeds.

Environment variables

export LOCALESS_ORIGIN=https://my-localess.web.app
export LOCALESS_SPACE=YOUR_SPACE_ID
export LOCALESS_TOKEN=YOUR_API_TOKEN

# Optional: bypass the platform compatibility check (any value)
export LOCALESS_SKIP_VERSION_CHECK=1

File-based credentials

// .localess/credentials.json
{
  "origin": "https://my-localess.web.app",
  "space": "YOUR_SPACE_ID",
  "token": "YOUR_API_TOKEN"
}

Never commit .localess/credentials.json. localess login adds .localess to .gitignore automatically.

localess translation was previously localess translations, and localess type was localess types. The old plural names still work as aliases.

localess translation push

Upload a local JSON translation file to Localess. Prints only the keys the selected --type acts on, as +/~/- lines under a summary such as Added 1 translation in locale "en". or, with --dry-run, Dry run: would add 1 translation in locale "en": (No translations to add for locale "en". when there is nothing to do). It does not print a full diff; use translation diff for that. update-existing, delete-missing-key and delete-missing-value first ask the server for a dry run, list the affected keys and prompt for confirmation (skippable with -y, --yes, automatically skipped under --dry-run, and nothing is pushed when the dry run reports no keys); add-missing never prompts since it is additive-only. Declining the prompt prints Aborted. and exits 1.

The server responds with { message, ids, dryRun? }, where ids lists only the keys the push type wrote (or would write).

localess translation push <locale> --path <file> [options]
Flag Default Description
-p, --path <path> required Path to the translations JSON file
-f, --format <format> flat File format: flat or nested
-t, --type <type> add-missing Update strategy (see below)
--dry-run false Preview changes without applying (also skips confirmation)
-y, --yes false Skip the confirmation prompt
-v, --verbose false Print verbose debug output

Update strategies

Strategy Behaviour Confirmation
add-missing Only adds keys absent from Localess Never
update-existing Only updates keys already in Localess — overwrites edits made in Localess since your last pull Prompted
delete-missing-key Deletes keys absent from the local file in every locale — run it with a complete file (normally the source locale) Prompted
delete-missing-value Removes only <locale>'s value of keys absent from the local file; other locales keep theirs Prompted

The two delete strategies differ in scope: delete-missing-key deletes keys from every locale — the pushed <locale> only selects the file — so run it with a complete file, normally the source locale; delete-missing-value removes only <locale>'s value and leaves other languages untouched. The preview, prompt and summary state the scope (… in every locale / the "de" value of … (other locales keep theirs)).

File formats

Flat (default):

{ "common.submit": "Submit", "nav.home": "Home" }

Nested (flattened automatically before uploading):

{ "common": { "submit": "Submit" }, "nav": { "home": "Home" } }

Examples

localess translation push en --path ./locales/en.json
localess translation push de --path ./locales/de.json --type update-existing
localess translation push de --path ./locales/de.json --type delete-missing-value
localess translation push en --path ./locales/en.json --type delete-missing-key
localess translation push fr --path ./locales/fr.json --dry-run
localess translation push de --path ./locales/de.json --format nested

localess translation pull

Download translations from Localess to a local JSON file. Keys are sorted alphabetically (recursively for nested) for stable, diff-friendly output. In nested, a key that is also the parent of other keys (button beside button.save) keeps its child keys; its own value is left out and listed in a warning. Use flat to keep every key.

By default the file is what the app is served: any key with no value in <locale> holds the fallback locale's text. That suits bundling translations into an app, but not files you edit and push back — push --type update-existing would save that fallback text as <locale> translations. Use --raw for those: it writes only the values actually stored for the locale, leaving gaps as gaps (needs a platform newer than 4.0.0, and the Development Tools permission). A locale the space doesn't have is refused rather than silently served as the fallback locale (checked when the token can read the space).

localess translation pull <locale> --path <file> [options]
Flag Default Description
-p, --path <path> required Output file path
-f, --format <format> flat File format: flat or nested
--draft false Pull the draft (unpublished) version
--raw false Pull only the values stored for the locale, without fallback filling — for files you edit and push back. Cannot be combined with --draft
-v, --verbose false Print verbose debug output
localess translation pull en --path ./locales/en.json
localess translation pull de --path ./locales/de.json --format nested
localess translation pull en --path ./locales/en.json --draft
localess translation pull de --path ./locales/de.json --raw

localess translation diff

Read-only comparison between a local translations file and the space — published by default, the draft with --draft, or the values stored for the locale with --raw (exactly what push acts on). Groups keys into Only in file/Different/Only in Localess sections (color-coded, git-diff style +/~/- symbols; direction-neutral labels); unchanged keys collapse into a count by default. Exits 1 on any drift — use as a CI gate. A locale the space doesn't have is refused (checked when the token can read the space).

localess translation diff <locale> --path <file> [options]
Flag Default Description
-p, --path <path> required Path to the local translations file
-f, --format <format> flat File format: flat or nested
--draft false Compare against the draft version
--raw false Compare against stored values, without fallback filling — what push acts on. Cannot be combined with --draft
-a, --all false Also print unchanged keys
-v, --verbose false Print verbose debug output
localess translation diff en --path ./locales/en.json
localess translation diff en --path ./locales/en.json --all
localess translation diff de --path ./locales/de.json --raw

Report layout: Only in file (n) / Different (n) / Only in Localess (n) sections (empty ones omitted), an N unchanged (use --all to show) line, then either N translation(s) differ: a only in file, b different, c only in Localess. or In sync..

Syncing translations

The translation commands are stateless building blocks: each has a fixed direction, and you choose the direction per locale and per step.

Source (pull, diff) Flag
published, fallback-filled (default)
draft, fallback-filled --draft
stored values, no fallback filling --raw
  • pull replaces the file with Localess's version — Localess wins.
  • push --type … applies one operation from the file — the file wins: add-missing, update-existing, delete-missing-key (every locale), delete-missing-value (this locale only).
  • diff shows Only in file / Different / Only in Localess against the same source pull would use and exits 1 on drift; diff --raw predicts exactly what push would act on.

Typical setups:

  • Code owns everything: push --type add-missing and update-existing per locale; diff --raw as a CI gate.
  • Localess owns everything: pull per locale at build time; translators edit in Localess.
  • Code owns keys and the source language, translators own the rest: push en --type add-missing / update-existing / delete-missing-key from the source file; pull <locale> for the other locales.

localess type generate

Generate TypeScript type definitions from your Localess space's schemas.

localess type generate [--path <output>] [--prefix <prefix>]
Flag Default Description
-p, --path <path> .localess/localess.d.ts Output file path
--prefix <prefix> '' Prefix prepended to every generated type name (including the helper types and ContentData)
-v, --verbose false Print verbose debug output

Prerequisite: The API token must have the Development Tools permission in Localess Space settings.

What it does:

  1. Fetches the schema definitions from your space (GET /schemas)
  2. Generates a single .d.ts file: ContentAsset/ContentLink/ContentReference/ContentRichText helper interfaces, one interface per ROOT/NODE schema (fields sorted by name; optional unless required: true; _id: string plus a literal _schema: '<schemaId>'), one string-literal union per ENUM schema, and a ContentData union of all ROOT types
localess type generate
localess type generate --path src/types/localess.d.ts
localess type generate --prefix Localess   # LocalessPage, LocalessContentAsset, LocalessContentData, ...

Generated output:

/**
 * Generated by Localess CLI
 * Do not edit manually.
 */
export interface ContentAsset { kind: 'ASSET'; uri: string; }
// ... ContentLink, ContentReference, ContentRichText

export interface Page {
  /** Unique identifier of a component in a content. */
  _id: string;
  /** Unique identifier for the Schema object. */
  _schema: 'page';
  body?: (HeroSection | CardGrid | RichTextBlock)[];
  title: string;
}

export type ContentData = Page;

Using generated types:

import type { Page } from './.localess/localess';
const content = await client.getContentBySlug<Page>('home');

Schema Commands

Define schemas in TypeScript with @localess/schema (defineSchema/defineEnum/defineConfig) and sync them with a space. See docs/schema.md for the authoring API.

Entry file: a TS/JS file exporting the result of defineConfig() (convention: schemas/index.ts).

Prerequisite: The token needs the Development Tools permission (DEV_TOOLS) — same as type generate. No dedicated schema permission exists.

localess schema validate <entry>

Offline — no login, no network. Loads the entry with jiti (default export first, then named exports; first value shaped like a defineConfig() result wins), prints each validate() issue as ERROR|WARNING <code> <path> — <message>; exits 1 on any error-severity issue or if no config is found. --format json prints the full ValidationResult instead.

localess schema validate ./schemas/index.ts
localess schema validate ./schemas/index.ts --format json

localess schema pull [--path <dir>] [--print-width <n>]

(Re)generates one TS definition file per schema (kebab-case name, e.g. HeroBlock → hero-block.ts) plus index.ts (export const config = defineConfig({ schemas: [...] })) from the space, into --path (default schemas). Repeatable: only overwrites/deletes files it previously generated (marked with a header comment); a same-named hand-written file without that marker is skipped and reported, never overwritten. Each field is emitted wrapped in defineField(...) rather than as a bare object literal (import { defineField, defineSchema } from '@localess/schema'; defineEnum for ENUMs). References to other pulled schemas (source, schemas) become import { X } from './x' statements and by-value refs; unknown ids stay strings. A defineField(...) call wraps to one property per line once it would exceed --print-width columns (default 80, Prettier's own default — set it to match your own project's .prettierrc, since that's a per-project preference this CLI doesn't assume). Output is deterministic for a given input and --print-width.

localess schema pull
localess schema pull --path src/schemas
localess schema pull --print-width 100

localess schema diff <entry>

Read-only comparison, same grouped/colored report layout as translation diff, with schema-specific Create/Update/Stale sections; unchanged schemas collapsed into a count by default. Equality is key-sorted JSON of each SchemaExport, matching the server's change detection. Exits 1 on any drift — use as a CI gate. Does not run validate() first (unlike push).

localess schema diff ./schemas/index.ts
localess schema diff ./schemas/index.ts --all   # also list unchanged schemas

localess schema push <entry> [--dry-run] [--delete] [-a] [-y]

Validates, diffs, then pushes. The pre-push diff uses the same grouped/colored report as schema diff (-a, --all to also list unchanged schemas).

localess schema push ./schemas/index.ts --dry-run   # preview
localess schema push ./schemas/index.ts             # upsert: create/update only
localess schema push ./schemas/index.ts --delete    # sync: also delete schemas absent from code (confirms unless -y)

Aborts (exit 1) without pushing if validation reports any error. Default is upsert (type: 'upsert' — stale server schemas are kept and listed in a warning); --delete sends type: 'sync' and prompts with the exact list unless -y, --dry-run, or nothing is stale. Prints the server's created/updated/deleted/unchanged counts (prefixed [DryRun] under --dry-run), then reconciles the pre-push diff against the returned ids and prints a ⚠ Prediction mismatch warning (without failing the command) for any schema id whose predicted status didn't match what the server actually did — e.g. a concurrent change made between the preview and the push. stale entries are only reconciled in sync mode.

CI/CD Integration

Use environment variables — no localess login step required.

# .github/workflows/sync-translations.yml
name: Sync translations
on:
  push:
    paths: ['locales/**']
jobs:
  push-translations:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24' }
      - run: npm install -g @localess/cli
      - run: localess translation push en --path ./locales/en.json
        env:
          LOCALESS_ORIGIN: ${{ secrets.LOCALESS_ORIGIN }}
          LOCALESS_SPACE: ${{ secrets.LOCALESS_SPACE_ID }}
          LOCALESS_TOKEN: ${{ secrets.LOCALESS_TOKEN }}

Generate types in CI:

      - run: localess type generate --path src/types/localess.d.ts
        env:
          LOCALESS_ORIGIN: ${{ secrets.LOCALESS_ORIGIN }}
          LOCALESS_SPACE: ${{ secrets.LOCALESS_SPACE_ID }}
          LOCALESS_TOKEN: ${{ secrets.LOCALESS_TOKEN }}

Gate merges on schema drift:

      - run: localess schema validate ./schemas/index.ts
      - run: localess schema diff ./schemas/index.ts
        env:
          LOCALESS_ORIGIN: ${{ secrets.LOCALESS_ORIGIN }}
          LOCALESS_SPACE: ${{ secrets.LOCALESS_SPACE_ID }}
          LOCALESS_TOKEN: ${{ secrets.LOCALESS_TOKEN }}

Local Development Workflow

# 1. Authenticate once
localess login

# 2. Generate types after schema changes in Localess CMS
localess type generate

# 3. Pull latest translations
localess translation pull en --path ./locales/en.json

# 4. Edit locally, then push (dry-run first)
localess translation push en --path ./locales/en.json --dry-run
localess translation push en --path ./locales/en.json

Files Written by the CLI

File Created by Permissions Purpose
.localess/credentials.json localess login 0o600 (owner only) Persisted auth credentials
.localess/localess.d.ts localess type generate Standard Generated TypeScript types (path via -p, --path)
schemas/<kebab-case-id>.ts, schemas/index.ts localess schema pull Standard Generated @localess/schema definitions (dir via -p, --path; each file starts with the pull marker comment)

.gitignore Notes

localess login automatically appends .localess to .gitignore. To commit generated types while protecting credentials:

.localess/credentials.json
# .localess/localess.d.ts  ← uncomment if regenerating in CI instead of committing