@lionrapid/cli 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +206 -0
- package/README.md +53 -4
- package/dist/adapters/json.d.ts +2 -2
- package/dist/adapters/json.js +4 -3
- package/dist/adapters/nested.d.ts +6 -0
- package/dist/adapters/nested.js +8 -0
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/yaml.js +3 -3
- package/dist/api-client.d.ts +151 -1
- package/dist/api-client.js +221 -8
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +448 -23
- package/dist/commands/context.d.ts +9 -0
- package/dist/commands/context.js +28 -2
- package/dist/commands/doctor.d.ts +2 -1
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/init.d.ts +44 -0
- package/dist/commands/init.js +0 -0
- package/dist/commands/login.d.ts +47 -0
- package/dist/commands/login.js +110 -0
- package/dist/commands/pull.d.ts +22 -0
- package/dist/commands/pull.js +140 -21
- package/dist/commands/push.d.ts +36 -0
- package/dist/commands/push.js +61 -16
- package/dist/commands/snapshot.js +3 -2
- package/dist/commands/status.d.ts +76 -0
- package/dist/commands/status.js +221 -0
- package/dist/commands/types.js +2 -1
- package/dist/commands/validate.d.ts +5 -2
- package/dist/commands/validate.js +15 -2
- package/dist/config.d.ts +41 -1
- package/dist/config.js +162 -8
- package/dist/job-history.d.ts +97 -0
- package/dist/job-history.js +175 -0
- package/dist/types-gen/extract.js +7 -1
- package/dist/types-gen/generate.js +9 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,212 @@
|
|
|
3
3
|
All notable changes to `@lionrapid/cli`. Format based on
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this package follows semver.
|
|
5
5
|
|
|
6
|
+
## Unreleased
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`push` reports the jobs it queued, and `lionrapid status` says what became
|
|
11
|
+
of them.** `sync/batch` answers `202 Accepted` — queued, not stored — and the
|
|
12
|
+
CLI discarded that response body at the transport layer (`syncBatch` returned
|
|
13
|
+
`void`). A push whose backend job later failed or dead-lettered was therefore
|
|
14
|
+
indistinguishable from one that worked: `pushed N string(s)` described the
|
|
15
|
+
CLI's own outbox and nothing else. Now:
|
|
16
|
+
|
|
17
|
+
- `syncBatch` returns the server's `BatchSyncJobResponse`, and `push` collects
|
|
18
|
+
every `job_ids` entry (not just the legacy `job_id`, so multi-locale batches
|
|
19
|
+
stay observable);
|
|
20
|
+
- `push` prints those ids with a `lionrapid status <job-id>` hint, and says so
|
|
21
|
+
explicitly when a server returns none;
|
|
22
|
+
- new **`lionrapid status [job-id|list]`**: one job's verdict from
|
|
23
|
+
`GET /sync/jobs/{id}` (status, error, retry history, dead-letter reason), or
|
|
24
|
+
the whole local job log newest-first, falling back to `/sync/queue-health`
|
|
25
|
+
and `/sync/dead-letter-queue` when this machine has nothing recorded.
|
|
26
|
+
|
|
27
|
+
- **Push receipts persist in `~/.lionrapid/jobs.json`.** Job id, project,
|
|
28
|
+
file(s), key count, push time and last-seen status, so `status` still answers
|
|
29
|
+
after the terminal is closed — and after the server's ~1h job retention has
|
|
30
|
+
purged the job. Terminal states are recorded and answered from disk with no
|
|
31
|
+
network call; anything unresolved is re-checked. Deliberately a separate file
|
|
32
|
+
from `credentials` (different lifetime and sensitivity — a corrupt history
|
|
33
|
+
must never cost you your login), 0600, written via temp-file + rename, and
|
|
34
|
+
pruned to 500 entries / 30 days.
|
|
35
|
+
|
|
36
|
+
- **Field-level drift guard on the sync response.** `BATCH_SYNC_RESPONSE_FIELDS`
|
|
37
|
+
plus a type-level assertion make a required server field missing from the
|
|
38
|
+
CLI's `SyncBatchResponse` a `tsc --noEmit` failure — `job_id`/`job_ids` were
|
|
39
|
+
silently dropped for exactly as long as nothing checked. (Guard lives in
|
|
40
|
+
`src/`, since tsconfig excludes tests from typechecking.)
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **A `GET` 404 is no longer retried as "sync already processing".** That
|
|
45
|
+
retry-on-404 heuristic is the enqueue path's; `GET /sync/jobs/{id}` returns the
|
|
46
|
+
same `SYNC_JOB_NOT_FOUND` code for a job that simply does not exist, which made
|
|
47
|
+
`status` back off three times before answering.
|
|
48
|
+
|
|
49
|
+
- **Every request carries `X-LR-Surface: cli`.** The server's surface registry
|
|
50
|
+
tells a customer which integration surfaces are live, but snippet, WordPress
|
|
51
|
+
and the CLI share the same project-wide key and were indistinguishable — so
|
|
52
|
+
each client now tags its requests with which surface it is. Older servers
|
|
53
|
+
ignore the header; no behavior changes.
|
|
54
|
+
|
|
55
|
+
## 0.2.2 — 2026-07-27
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`files[].flat` — dotted keys round-trip as dotted.** JSON/YAML targets are
|
|
60
|
+
written nested by default, so a key pushed flat (`hero.title`) came back as
|
|
61
|
+
`{"hero": {"title": …}}` — breaking flat-keyed consumers (CMS imports,
|
|
62
|
+
webhook payloads) that fed the file back in. `flat: true` on a `files` entry
|
|
63
|
+
keeps dotted keys literal on `pull`. Only valid for `json`/`yaml` (every
|
|
64
|
+
other format is flat by nature); glob entries carry the flag onto each
|
|
65
|
+
expanded file.
|
|
66
|
+
|
|
67
|
+
- **`lionrapid login` / `lionrapid logout`.** The credentials file the CLI
|
|
68
|
+
reads finally has an author: `login` verifies the key against the server
|
|
69
|
+
_before_ persisting (a failed ping writes nothing), writes
|
|
70
|
+
`~/.lionrapid/credentials` `0600` in a `0700` dir, and merges entries — bare
|
|
71
|
+
`login` writes `default`, `--server <url>` writes the server-scoped entry
|
|
72
|
+
that 0.2.1's trust boundary requires for non-LionRapid hosts. Omitting
|
|
73
|
+
`--key` gives a hidden interactive prompt; the key is never echoed. `logout`
|
|
74
|
+
removes an entry and deletes the file when empty. At read time the CLI now
|
|
75
|
+
warns (once per file) when the credentials file is world-readable.
|
|
76
|
+
- **Glob file entries with derived namespaces.** `files[].source` may be a
|
|
77
|
+
glob (`locales/en/*.json`); `target` and `namespace` then carry a
|
|
78
|
+
`{basename}` token substituted per matched file with its extension-less
|
|
79
|
+
name. One stanza replaces N hand-written entries (e.g. one file per page
|
|
80
|
+
slug → one namespace per page). Expansion happens at config load, is
|
|
81
|
+
deterministic (sorted), and a glob matching nothing warns instead of
|
|
82
|
+
silently doing nothing. A glob without `{basename}` in _both_ `target` and
|
|
83
|
+
`namespace` is rejected as ambiguous.
|
|
84
|
+
- **Per-attempt request timeouts.** Every request carries a fresh
|
|
85
|
+
`AbortSignal.timeout` budget (default 30 s) so a hung connection can no
|
|
86
|
+
longer block `push`/`pull`/`doctor` forever. Timeouts are retried like other
|
|
87
|
+
transient failures and report "timed out after Xms". Tune via
|
|
88
|
+
`LIONRAPID_TIMEOUT` or the global `--timeout <ms>` flag.
|
|
89
|
+
- **`validate --allow-locked`** downgrades locked (unapproved) namespaces to a
|
|
90
|
+
warning so brand-new namespaces don't fail CI.
|
|
91
|
+
- **Modern `init`: layout detection, prompts, and locale prefill.** `init`
|
|
92
|
+
detects a per-namespace `locales/en/<ns>.json` layout and scaffolds the glob
|
|
93
|
+
stanza for it (flat layouts keep the concrete entry). On a TTY it prompts
|
|
94
|
+
for the project label, server URL, and — new — an optional API key
|
|
95
|
+
(`--key`, stored credentials, or a hidden prompt) used to prefill
|
|
96
|
+
`sourceLocale`/`targetLocales` from the project's dashboard configuration
|
|
97
|
+
via `GET /api/integrations/project-config`. Everything degrades gracefully:
|
|
98
|
+
non-TTY runs never block, and offline/older servers fall back to the static
|
|
99
|
+
defaults. A project that simply has **no target languages yet** is reported
|
|
100
|
+
as exactly that — with the remediation (add them in the dashboard, re-run
|
|
101
|
+
`init --force`) and a warning that the scaffolded `targetLocales` are
|
|
102
|
+
placeholders — rather than being misreported as an unreachable server. The
|
|
103
|
+
overwrite guard fires before any prompt, and next-steps route credentials
|
|
104
|
+
through `lionrapid login` (and skip that advice when a stored credential
|
|
105
|
+
was already used).
|
|
106
|
+
- **`init` re-runs safely and stops asking for what it already knows.** It
|
|
107
|
+
defaults its server to the repo's config or the one you logged into (so
|
|
108
|
+
`lionrapid login` then a bare `lionrapid init` is the whole setup), and
|
|
109
|
+
re-running it is no longer an error: newly-detected files and locales merge
|
|
110
|
+
in and are listed, anything that would _change_ a value you set is reported
|
|
111
|
+
as a conflict (`yours:` / `project:`) and left untouched, and a run with
|
|
112
|
+
nothing new leaves the file byte-identical — hand-edits and comments
|
|
113
|
+
survive. `--force` still rewrites wholesale.
|
|
114
|
+
- **`init` no longer invents locales.** When neither the project nor the repo
|
|
115
|
+
yields target languages, `targetLocales` is written empty with a warning
|
|
116
|
+
saying so, instead of scaffolding a fabricated `[es, fr]` that produced
|
|
117
|
+
phantom pending keys on every later `pull`/`validate`.
|
|
118
|
+
- **`init` detects `.po`, XLIFF and standalone YAML catalogs.** Detection is
|
|
119
|
+
no longer limited to Flutter/iOS/Android/Rails/JSON: conventional catalog
|
|
120
|
+
roots (`po/`, `locale/`, `locales/`, `translations/`, `i18n/`, `src/locale/`,
|
|
121
|
+
`Content/Localization/`, …) are searched for `.po`, `.xlf`/`.xliff` and
|
|
122
|
+
`.yml`/`.yaml`, and the source→target stanza is derived from the files that
|
|
123
|
+
actually exist — covering gettext `locale/{locale}/LC_MESSAGES/…`, flat
|
|
124
|
+
`po/{locale}.po`, locale-suffixed `messages.{locale}.xlf`, and Unreal/Godot's
|
|
125
|
+
`Content/Localization/<Target>/{locale}/…`. Formats accumulate, so a repo
|
|
126
|
+
with both JSON and PO trees gets both stanzas.
|
|
127
|
+
- **`init` detects the locales already in your repo.** Locale resolution is
|
|
128
|
+
now a ladder — explicit `--source`/`--targets`, then the project's server
|
|
129
|
+
config, then locales found on disk by matching the detected `target`
|
|
130
|
+
pattern (`locales/{locale}/…`, `app_{locale}.arb`, `values-{locale}/…`,
|
|
131
|
+
`{locale}.lproj/…`), then static placeholders. Offline or on a project with
|
|
132
|
+
no targets yet, `init` scaffolds what the repo actually has instead of
|
|
133
|
+
guessing. When the server _does_ have targets it stays authoritative, and a
|
|
134
|
+
locale that exists only on disk is reported as a mismatch (`present in your
|
|
135
|
+
files but not configured on the project`) rather than silently merged —
|
|
136
|
+
those files would never be filled by `pull`.
|
|
137
|
+
|
|
138
|
+
### Changed
|
|
139
|
+
|
|
140
|
+
- **`push` makes partial progress instead of aborting.** One failing
|
|
141
|
+
file/namespace (a rate limit on a many-namespace site, one corrupt source
|
|
142
|
+
file) no longer zeroes the run: the remaining files still upload, failures
|
|
143
|
+
render per file with a summary and appear as `error` fields in the `--json`
|
|
144
|
+
envelope (`ok: false`), and the exit code is 5 (PARTIAL). Because the sync
|
|
145
|
+
endpoints upsert by composite key, re-running `push` retries just the
|
|
146
|
+
failures. When every attempted file fails, the first error is rethrown with
|
|
147
|
+
the usual exit taxonomy.
|
|
148
|
+
- **A `429` without `Retry-After` now waits a rate-limit window (30 s), not
|
|
149
|
+
millisecond jitter.** The server's rate-limit responses carry no
|
|
150
|
+
`Retry-After` header and its limits are per-minute windows — the old jittered
|
|
151
|
+
backoff (~2 s total across all retries) could never outlive one, so every
|
|
152
|
+
429 exhausted its retries and was effectively fatal. Concretely: a site with
|
|
153
|
+
more than 30 namespaces died mid-push at the sync endpoint's 30/min limit;
|
|
154
|
+
now push rides the window out and completes.
|
|
155
|
+
- **`pull` makes partial progress instead of aborting.** On a multi-namespace
|
|
156
|
+
pull, a failing namespace (or a corrupt local target) no longer zeroes the
|
|
157
|
+
run: successful files still write, failures render per file with a summary
|
|
158
|
+
and appear as `error` fields in the `--json` envelope (`ok: false`), and the
|
|
159
|
+
exit code is 5 (PARTIAL). When every file fails, the first error is rethrown
|
|
160
|
+
with the usual exit taxonomy.
|
|
161
|
+
- **`login` and `doctor` verify via `project-config` (`config:read`).** The
|
|
162
|
+
previous probe (`editor/workflow`) requires `targets:write`, which
|
|
163
|
+
publishable keys don't carry — so pk\_ keys failed verification even though
|
|
164
|
+
the whole standard loop (push/pull/validate/prefill) runs on them. Doctor's
|
|
165
|
+
server check now also echoes the project's configured locales.
|
|
166
|
+
- **`pull` is non-destructive by default.** Existing targets are merged, not
|
|
167
|
+
overwritten: the server wins per key, local-only keys and comments survive
|
|
168
|
+
(comments even on server-updated keys), existing key order is preserved and
|
|
169
|
+
new keys append. Each file reports `{added, updated, keptLocal}` diff counts
|
|
170
|
+
in prose and `--json`; `--dry-run` computes the diff without writing.
|
|
171
|
+
`pull --force` restores the previous wholesale overwrite.
|
|
172
|
+
- **`validate` reports a locked (423) namespace as one namespace-level
|
|
173
|
+
`locked` issue** per locale instead of N spurious `missing` lines. Locked
|
|
174
|
+
still fails `--ci` by default. Envelope note: `issues[].key` is now optional
|
|
175
|
+
and `locked` joins the issue types.
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
|
|
179
|
+
- **Nested keys were silently lost on `pull`.** The endpoint accepts flat
|
|
180
|
+
dotted keys on write but answers **nested** on read (a pushed `nav.cart`
|
|
181
|
+
comes back as `{"nav": {"cart": …}}`, because the SDK resolves nested
|
|
182
|
+
paths). Every CLI adapter is built on flat keys, so whole subtrees vanished:
|
|
183
|
+
`pull` wrote fewer keys than it pushed and `validate` reported the missing
|
|
184
|
+
ones forever. `getNamespace` now flattens the tree back to `dotted.key`
|
|
185
|
+
using the server's own leaf rule (a string, or an object carrying `text`, is
|
|
186
|
+
a value; any other object is a container).
|
|
187
|
+
- **`validate` no longer crashes on a malformed server value.** Reading
|
|
188
|
+
`.text` off a value that wasn't a ContentUnit threw
|
|
189
|
+
`Cannot read properties of undefined (reading 'trim')` and exit 1 — an
|
|
190
|
+
uncategorized crash on a normal run. The same normalization pass wraps bare
|
|
191
|
+
strings into `{ text, styles: {} }`, defaults a missing `styles`, and drops
|
|
192
|
+
a genuinely unusable leaf with a warning; `validate` treats an unusable
|
|
193
|
+
value as `empty` rather than assuming shape.
|
|
194
|
+
- **Clear error for keys with non-header-safe characters.** Pasting the
|
|
195
|
+
dashboard's truncated key display (`sk_eng_…last4`) used to surface as a
|
|
196
|
+
misleading "server could not be reached" network error — the U+2026
|
|
197
|
+
ellipsis can't be encoded into the `Authorization` header. The CLI now
|
|
198
|
+
rejects such keys at every resolution point (env, credentials file,
|
|
199
|
+
`login`) with exit 3, naming the offending character and the likely
|
|
200
|
+
truncated-copy cause, before any request is sent.
|
|
201
|
+
- **Security — codegen injection in `types generate`.** With `--server`, keys
|
|
202
|
+
and select-option labels are server-controlled; labels are now whitelisted
|
|
203
|
+
to `[A-Za-z0-9_]+` before reaching type position (non-conforming ones drop,
|
|
204
|
+
all-hostile falls back to `string`) and key escaping `\u`-escapes control
|
|
205
|
+
characters and JS line terminators, so no remote string can inject
|
|
206
|
+
declarations into the generated `.d.ts`.
|
|
207
|
+
- **Security — path containment for config-derived writes.** Pull targets,
|
|
208
|
+
multi-locale catalogs, snapshot outputs, and the generated `.d.ts` path must
|
|
209
|
+
resolve inside the project directory; `../` escapes and absolute paths are
|
|
210
|
+
rejected with exit 2 naming the offending path.
|
|
211
|
+
|
|
6
212
|
## 0.2.1 — 2026-07-20
|
|
7
213
|
|
|
8
214
|
Security batch (PLAN items 19–23).
|
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ Phase 1 (file-sync dev-loop) is implemented. Command surface:
|
|
|
22
22
|
| ----------------- | ------ | ------------------------------------------------------------------- |
|
|
23
23
|
| `init` | ✅ | Scaffold a `lionrapid.yml` (detects Flutter/iOS/Android/Rails/JS) |
|
|
24
24
|
| `push` | ✅ | Upload source strings to the backend (server handles MT) |
|
|
25
|
+
| `status [job-id]` | ✅ | What the backend actually did with what `push` queued |
|
|
25
26
|
| `pull [--types]` | ✅ | Download translations into target files; optionally regen types |
|
|
26
27
|
| `validate [--ci]` | ✅ | Check local files vs the backend for missing/orphan/empty keys |
|
|
27
28
|
| `types generate` | ✅ | Generate a typed `.d.ts` from your source files (offline) |
|
|
@@ -47,6 +48,35 @@ files:
|
|
|
47
48
|
- { format: json, source: app/en.json, target: app/{locale}.json } # → namespace: app
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
**Glob entries:** `source` may be a glob (`*`/`?`); `target` and `namespace`
|
|
52
|
+
must then carry a `{basename}` token, replaced per matched file with its
|
|
53
|
+
extension-less name. One stanza covers a whole directory of per-page (or
|
|
54
|
+
per-feature) catalogs, each in its own namespace:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
files:
|
|
58
|
+
- format: json
|
|
59
|
+
source: locales/en/*.json # locales/en/home.json, pricing.json, …
|
|
60
|
+
target: locales/{locale}/{basename}.json
|
|
61
|
+
namespace: '{basename}' # → namespaces home, pricing, …
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Globs expand once at config load (sorted, so behavior is deterministic) —
|
|
65
|
+
`push`/`pull`/`validate` see one concrete entry per matched file, and `doctor`
|
|
66
|
+
reports the expanded count. A glob that matches nothing prints a warning
|
|
67
|
+
instead of silently doing nothing; a glob entry without `{basename}` in _both_
|
|
68
|
+
`target` and `namespace` is rejected as ambiguous.
|
|
69
|
+
|
|
70
|
+
**Flat output:** JSON/YAML targets are written nested by default (a pushed
|
|
71
|
+
`hero.title` comes back as `{"hero": {"title": …}}`). Set `flat: true` on a
|
|
72
|
+
`files` entry to keep dotted keys literal on `pull`, so a flat-keyed file
|
|
73
|
+
round-trips flat for flat-keyed consumers (CMS imports, webhook payloads):
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
files:
|
|
77
|
+
- { format: json, source: locales/en.json, target: locales/{locale}.json, flat: true }
|
|
78
|
+
```
|
|
79
|
+
|
|
50
80
|
## Reliability & automation
|
|
51
81
|
|
|
52
82
|
Built for CI and AI agents — every networked command is resilient and scriptable.
|
|
@@ -61,9 +91,26 @@ Built for CI and AI agents — every networked command is resilient and scriptab
|
|
|
61
91
|
- **Exit codes** (stable, gate CI on them): `0` ok · `1` unexpected · `2`
|
|
62
92
|
validation/config · `3` auth · `4` network/5xx · `5` partial.
|
|
63
93
|
- **Automatic retries**: transient failures (network error, `5xx`, `429`) are
|
|
64
|
-
retried with exponential backoff + jitter
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
retried with exponential backoff + jitter. A `429` honors `Retry-After` when
|
|
95
|
+
the server sends it; when it doesn't, the CLI waits a rate-limit-window 30 s
|
|
96
|
+
instead of jittering, so a many-namespace push rides out a per-minute limit.
|
|
97
|
+
Tune with `LIONRAPID_RETRIES` (default `3`; `0` disables). Writes are safe to
|
|
98
|
+
retry — the integration endpoints upsert by composite key.
|
|
99
|
+
- **Partial progress** (`push`, `pull`): one failing namespace or file doesn't
|
|
100
|
+
zero the run — the rest complete, failures are listed per file (and as
|
|
101
|
+
`error` fields under `--json`), and the exit code is `5` (partial).
|
|
102
|
+
Re-running retries just the failures; pushes upsert, so that's always safe.
|
|
103
|
+
- **Pushed ≠ stored (`push` → `status`)**: `sync/batch` answers `202 Accepted`,
|
|
104
|
+
so `push`'s counts are what the CLI **sent**; the backend stores them in a
|
|
105
|
+
background job that can still fail or dead-letter. `push` therefore prints the
|
|
106
|
+
job ids it queued and saves them to `~/.lionrapid/jobs.json`, and
|
|
107
|
+
`lionrapid status <job-id>` reports the server's verdict — stored, still
|
|
108
|
+
queued, retrying, or dead-lettered (with the reason and every failed attempt).
|
|
109
|
+
Bare `lionrapid status` lists every job this machine pushed, newest first,
|
|
110
|
+
re-checking only the unresolved ones; with nothing recorded it falls back to
|
|
111
|
+
the server's queue health + dead-letter queue. Offline, it still prints the
|
|
112
|
+
local record, labelled `(last known)`. History is capped at 500 entries /
|
|
113
|
+
30 days and never shares a file with your credentials.
|
|
67
114
|
- **Concurrent `pull`**: namespace fetches fan out with bounded concurrency
|
|
68
115
|
(`LIONRAPID_CONCURRENCY`, default `6`) and show interactive progress on a TTY.
|
|
69
116
|
Output stays deterministic — only the network fan-out is parallelized.
|
|
@@ -143,7 +190,9 @@ already translated on your website are translated in the app for free.
|
|
|
143
190
|
|
|
144
191
|
```bash
|
|
145
192
|
lionrapid init --project <id> --targets es,fr
|
|
146
|
-
lionrapid push
|
|
193
|
+
lionrapid push # prints the job ids it queued
|
|
194
|
+
lionrapid status # …and what the backend did with them
|
|
195
|
+
lionrapid status <job-id> # the full story on one job
|
|
147
196
|
lionrapid pull --types
|
|
148
197
|
lionrapid validate --ci
|
|
149
198
|
|
package/dist/adapters/json.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* node backends). Nested objects flatten to dot-notation keys (`home.cta`) and
|
|
4
4
|
* unflatten back on serialize, preserving insertion order.
|
|
5
5
|
*/
|
|
6
|
-
import type { FormatAdapter, TranslationEntry } from './types.js';
|
|
6
|
+
import type { FormatAdapter, SerializeOptions, TranslationEntry } from './types.js';
|
|
7
7
|
export declare class JsonAdapter implements FormatAdapter {
|
|
8
8
|
readonly format: "json";
|
|
9
9
|
parse(content: string): TranslationEntry[];
|
|
10
|
-
serialize(entries: TranslationEntry[]): string;
|
|
10
|
+
serialize(entries: TranslationEntry[], options?: SerializeOptions): string;
|
|
11
11
|
}
|
|
12
12
|
//# sourceMappingURL=json.d.ts.map
|
package/dist/adapters/json.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* node backends). Nested objects flatten to dot-notation keys (`home.cta`) and
|
|
4
4
|
* unflatten back on serialize, preserving insertion order.
|
|
5
5
|
*/
|
|
6
|
-
import { flattenObject, unflattenEntries } from './nested.js';
|
|
6
|
+
import { flatEntriesObject, flattenObject, unflattenEntries } from './nested.js';
|
|
7
7
|
export class JsonAdapter {
|
|
8
8
|
format = 'json';
|
|
9
9
|
parse(content) {
|
|
@@ -24,8 +24,9 @@ export class JsonAdapter {
|
|
|
24
24
|
flattenObject(data, '', out);
|
|
25
25
|
return out;
|
|
26
26
|
}
|
|
27
|
-
serialize(entries) {
|
|
28
|
-
|
|
27
|
+
serialize(entries, options) {
|
|
28
|
+
const tree = options?.flat ? flatEntriesObject(entries) : unflattenEntries(entries);
|
|
29
|
+
return `${JSON.stringify(tree, null, 2)}\n`;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
//# sourceMappingURL=json.js.map
|
|
@@ -5,5 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { TranslationEntry } from './types.js';
|
|
7
7
|
export declare function flattenObject(obj: Record<string, unknown>, prefix: string, out: TranslationEntry[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* Flat serialize: dotted keys stay literal (`"hero.title"`), nothing re-nests.
|
|
10
|
+
* Built with Object.fromEntries so a hostile key like `__proto__` lands as a
|
|
11
|
+
* plain own property instead of touching the prototype.
|
|
12
|
+
*/
|
|
13
|
+
export declare function flatEntriesObject(entries: TranslationEntry[]): Record<string, unknown>;
|
|
8
14
|
export declare function unflattenEntries(entries: TranslationEntry[], onSkippedKey?: (key: string) => void): Record<string, unknown>;
|
|
9
15
|
//# sourceMappingURL=nested.d.ts.map
|
package/dist/adapters/nested.js
CHANGED
|
@@ -18,6 +18,14 @@ export function flattenObject(obj, prefix, out) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Flat serialize: dotted keys stay literal (`"hero.title"`), nothing re-nests.
|
|
23
|
+
* Built with Object.fromEntries so a hostile key like `__proto__` lands as a
|
|
24
|
+
* plain own property instead of touching the prototype.
|
|
25
|
+
*/
|
|
26
|
+
export function flatEntriesObject(entries) {
|
|
27
|
+
return Object.fromEntries(entries.map((e) => [e.key, fromContentUnit(e.value)]));
|
|
28
|
+
}
|
|
21
29
|
/** Path segments that would write onto the prototype chain instead of plain data. */
|
|
22
30
|
const UNSAFE_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
23
31
|
function warnSkippedKey(key) {
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface SerializeOptions {
|
|
|
30
30
|
locale?: string;
|
|
31
31
|
/** Source locale, recorded by bilingual formats (e.g. XLIFF `source-language`). */
|
|
32
32
|
sourceLocale?: string;
|
|
33
|
+
/** Keep dotted keys flat (JSON/YAML) instead of re-nesting them into objects. */
|
|
34
|
+
flat?: boolean;
|
|
33
35
|
}
|
|
34
36
|
export interface FormatAdapter {
|
|
35
37
|
readonly format: FileFormat;
|
package/dist/adapters/yaml.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* JSON adapter; this adapter assumes the Rails/locale-rooted shape.)
|
|
9
9
|
*/
|
|
10
10
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
11
|
-
import { flattenObject, unflattenEntries } from './nested.js';
|
|
11
|
+
import { flatEntriesObject, flattenObject, unflattenEntries } from './nested.js';
|
|
12
12
|
function isObject(v) {
|
|
13
13
|
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
14
14
|
}
|
|
@@ -43,8 +43,8 @@ export class YamlAdapter {
|
|
|
43
43
|
return out;
|
|
44
44
|
}
|
|
45
45
|
serialize(entries, options) {
|
|
46
|
-
const
|
|
47
|
-
const doc = options?.locale ? { [options.locale]:
|
|
46
|
+
const tree = options?.flat ? flatEntriesObject(entries) : unflattenEntries(entries);
|
|
47
|
+
const doc = options?.locale ? { [options.locale]: tree } : tree;
|
|
48
48
|
return stringifyYaml(doc);
|
|
49
49
|
}
|
|
50
50
|
}
|
package/dist/api-client.d.ts
CHANGED
|
@@ -14,6 +14,90 @@ export interface KeyedTranslation {
|
|
|
14
14
|
key: string;
|
|
15
15
|
value: ContentUnit;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The 202 body of `POST /api/integrations/sync/batch` — a mirror of the
|
|
19
|
+
* server's `BatchSyncJobResponse` (tm-server
|
|
20
|
+
* `backend/app/domains/integrations/contracts/responses.py`). Field names are
|
|
21
|
+
* the wire names on purpose: this type IS the contract, and
|
|
22
|
+
* {@link BATCH_SYNC_RESPONSE_FIELDS} guards it against server drift.
|
|
23
|
+
*
|
|
24
|
+
* 202 means **queued, not stored**. The rows only reach the database when the
|
|
25
|
+
* job queued here actually runs, so a push that returns 202 for every batch is
|
|
26
|
+
* still compatible with the backend later failing or dead-lettering the work.
|
|
27
|
+
* The job ids are the only way a client can tell those apart — discarding this
|
|
28
|
+
* body (as `syncBatch` used to) is what made a failed sync indistinguishable
|
|
29
|
+
* from a successful one.
|
|
30
|
+
*/
|
|
31
|
+
export interface SyncBatchResponse {
|
|
32
|
+
/** First queued job id — the server's legacy alias for `job_ids[0]`. */
|
|
33
|
+
job_id: string;
|
|
34
|
+
/** ALL queued job ids — one per locale group in the batch. */
|
|
35
|
+
job_ids: string[];
|
|
36
|
+
/** Job status at enqueue time (`pending`). */
|
|
37
|
+
status: string;
|
|
38
|
+
/** Number of keys the server accepted into the batch. */
|
|
39
|
+
total_keys: number;
|
|
40
|
+
/** Human-readable message — carries the server's own poll hint. */
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The REQUIRED fields of the server's `BatchSyncJobResponse`, transcribed from
|
|
45
|
+
* `contracts/responses.py` (a field with no default is required there).
|
|
46
|
+
*
|
|
47
|
+
* DRIFT GUARD, field level. `job_id`/`job_ids` existed server-side the whole
|
|
48
|
+
* time and the CLI silently dropped them — a `Promise<void>` cannot be caught
|
|
49
|
+
* by any test, because there is nothing to assert on. This list plus
|
|
50
|
+
* {@link SyncBatchResponseCoversContract} makes that failure a *compile* error:
|
|
51
|
+
* add a required field here without adding it to {@link SyncBatchResponse} and
|
|
52
|
+
* `npm run typecheck` fails, so the next server field can't vanish the same way.
|
|
53
|
+
*
|
|
54
|
+
* (Deliberately not derived from the server's OpenAPI: tm-server is a separate
|
|
55
|
+
* repo, so that check would need a network fetch or a snapshot to drift on its
|
|
56
|
+
* own. Hand-transcribing five names is the cheaper, non-flaky half.)
|
|
57
|
+
*/
|
|
58
|
+
export declare const BATCH_SYNC_RESPONSE_FIELDS: readonly ["job_id", "job_ids", "status", "total_keys", "message"];
|
|
59
|
+
/** Compiles only when `T` is `never` — i.e. no contract field is unaccounted for. */
|
|
60
|
+
type NoMissingContractFields<T extends never> = T;
|
|
61
|
+
/**
|
|
62
|
+
* The guard itself. Exported as a type (not a value) so it costs no runtime and
|
|
63
|
+
* no lint suppression, and it is checked because `tsc --noEmit` covers `src/`.
|
|
64
|
+
*/
|
|
65
|
+
export type SyncBatchResponseCoversContract = NoMissingContractFields<Exclude<(typeof BATCH_SYNC_RESPONSE_FIELDS)[number], keyof SyncBatchResponse>>;
|
|
66
|
+
/**
|
|
67
|
+
* A sync job's server-side state — `GET /api/integrations/sync/jobs/{id}`
|
|
68
|
+
* (`SyncJobStatusResponse`). Camel-cased because, unlike
|
|
69
|
+
* {@link SyncBatchResponse}, this is the CLI's own view rather than a guarded
|
|
70
|
+
* contract mirror.
|
|
71
|
+
*
|
|
72
|
+
* `status` is one of `pending` / `processing` / `retry_pending` / `completed` /
|
|
73
|
+
* `failed`. The server only ever sets `failed` from its dead-letter path, so a
|
|
74
|
+
* `failed` job is one the backend gave up on: those rows were never stored.
|
|
75
|
+
*/
|
|
76
|
+
export interface SyncJobStatus {
|
|
77
|
+
jobId: string;
|
|
78
|
+
status: string;
|
|
79
|
+
createdAt?: string;
|
|
80
|
+
startedAt?: string;
|
|
81
|
+
completedAt?: string;
|
|
82
|
+
/** Sync outcome on success (`keys_added`, `keys_updated`, …). */
|
|
83
|
+
result?: Record<string, unknown>;
|
|
84
|
+
error?: string;
|
|
85
|
+
retryCount: number;
|
|
86
|
+
maxRetries: number;
|
|
87
|
+
nextRetryAt?: string;
|
|
88
|
+
/** One entry per failed attempt, oldest first. */
|
|
89
|
+
retryErrors: string[];
|
|
90
|
+
isDeadLettered: boolean;
|
|
91
|
+
deadLetterReason?: string;
|
|
92
|
+
}
|
|
93
|
+
/** Queue-wide metrics — `GET /api/integrations/sync/queue-health`. */
|
|
94
|
+
export interface QueueHealth {
|
|
95
|
+
totalJobs: number;
|
|
96
|
+
queueDepth: number;
|
|
97
|
+
statusCounts: Record<string, number>;
|
|
98
|
+
dlqCount: number;
|
|
99
|
+
workerRunning: boolean;
|
|
100
|
+
}
|
|
17
101
|
/** Result of a namespace pull. `notModified` is true on a 304 (ETag hit). */
|
|
18
102
|
export interface NamespaceBundle {
|
|
19
103
|
translations: Record<string, ContentUnit>;
|
|
@@ -41,9 +125,17 @@ export interface ApiClientOptions {
|
|
|
41
125
|
retries?: number;
|
|
42
126
|
/** Injectable delay (defaults to real `setTimeout`). Tests pass a no-op. */
|
|
43
127
|
sleep?: (ms: number) => Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Per-attempt request timeout in ms (each retry gets a fresh budget).
|
|
130
|
+
* Default {@link DEFAULT_TIMEOUT_MS}. A hung connection must never block
|
|
131
|
+
* push/pull/doctor forever — poison for CI.
|
|
132
|
+
*/
|
|
133
|
+
timeoutMs?: number;
|
|
44
134
|
}
|
|
45
135
|
/** Default number of retries for transient failures. */
|
|
46
136
|
export declare const DEFAULT_RETRIES = 3;
|
|
137
|
+
/** Default per-attempt request timeout. */
|
|
138
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
47
139
|
/**
|
|
48
140
|
* Parse a `Retry-After` header → ms, or null if absent/unparseable. Accepts the
|
|
49
141
|
* delta-seconds form (`120`) and the HTTP-date form; clamps to
|
|
@@ -68,6 +160,31 @@ export declare class ApiError extends Error {
|
|
|
68
160
|
export declare class NetworkError extends Error {
|
|
69
161
|
constructor(method: string, path: string, cause: unknown);
|
|
70
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Coerce a namespace payload into flat `dotted.key` → {@link ContentUnit}.
|
|
165
|
+
*
|
|
166
|
+
* The endpoint takes flat keys on write but answers **nested** on read
|
|
167
|
+
* (`{"nav": {"cart": …}}` for a pushed `nav.cart`), because the SDK resolves
|
|
168
|
+
* nested paths. Every CLI adapter is built on flat dotted keys, so the tree
|
|
169
|
+
* has to be flattened back here or whole subtrees silently vanish. The leaf
|
|
170
|
+
* rule mirrors the server's: a string, or an object carrying `text`, is a
|
|
171
|
+
* value; any other object is a container to descend into. Bare strings are
|
|
172
|
+
* wrapped, a missing `styles` defaults to `{}`, and a genuinely unusable leaf
|
|
173
|
+
* is dropped with a warning rather than crashing the run.
|
|
174
|
+
*/
|
|
175
|
+
export declare function normalizeTranslations(raw: unknown): Record<string, ContentUnit>;
|
|
176
|
+
/**
|
|
177
|
+
* Project the sync/batch body onto {@link SyncBatchResponse}, field by field.
|
|
178
|
+
*
|
|
179
|
+
* Written out explicitly rather than cast, so a dropped field is a *runtime*
|
|
180
|
+
* regression a test can see as well as a compile error (see
|
|
181
|
+
* {@link BATCH_SYNC_RESPONSE_FIELDS}). Every field degrades instead of throwing:
|
|
182
|
+
* an older server, a proxy that eats the body, or a 204 must leave push working
|
|
183
|
+
* — you simply get no job ids to report, which the CLI says out loud.
|
|
184
|
+
*/
|
|
185
|
+
export declare function normalizeSyncBatchResponse(raw: unknown): SyncBatchResponse;
|
|
186
|
+
/** Project a `SyncJobStatusResponse` (job status, or a DLQ entry) onto {@link SyncJobStatus}. */
|
|
187
|
+
export declare function normalizeSyncJob(raw: unknown): SyncJobStatus;
|
|
71
188
|
export declare class LionRapidApiClient {
|
|
72
189
|
private readonly baseUrl;
|
|
73
190
|
private readonly apiKey;
|
|
@@ -75,6 +192,7 @@ export declare class LionRapidApiClient {
|
|
|
75
192
|
private readonly fetchImpl;
|
|
76
193
|
private readonly retries;
|
|
77
194
|
private readonly sleep;
|
|
195
|
+
private readonly timeoutMs;
|
|
78
196
|
constructor(opts: ApiClientOptions);
|
|
79
197
|
/** True when this client holds an editor token and can publish (mark-ready). */
|
|
80
198
|
get canPublish(): boolean;
|
|
@@ -82,11 +200,42 @@ export declare class LionRapidApiClient {
|
|
|
82
200
|
ping(): Promise<{
|
|
83
201
|
isPmAdmin: boolean;
|
|
84
202
|
}>;
|
|
203
|
+
/**
|
|
204
|
+
* The project's dashboard-configured locales (for `init` prefill).
|
|
205
|
+
* GET /api/integrations/project-config — flat `{ defaultLocale, languages }`;
|
|
206
|
+
* `languages` includes the default locale. Malformed bodies normalize to
|
|
207
|
+
* empty values rather than throwing.
|
|
208
|
+
*/
|
|
209
|
+
getProjectConfig(): Promise<{
|
|
210
|
+
defaultLocale: string;
|
|
211
|
+
languages: string[];
|
|
212
|
+
}>;
|
|
85
213
|
/**
|
|
86
214
|
* Register source-row text for `sourceLocale`. Mirrors what the edge does on
|
|
87
215
|
* first visit when it encounters a missing key. POST /api/integrations/sync/batch.
|
|
216
|
+
*
|
|
217
|
+
* Returns the server's 202 body. It used to return `void` — the response was
|
|
218
|
+
* read and thrown away at the transport layer, which is why a push whose jobs
|
|
219
|
+
* later died looked exactly like one that worked. Callers get the job ids and
|
|
220
|
+
* can poll {@link getSyncJob}.
|
|
221
|
+
*/
|
|
222
|
+
syncBatch(sourceLocale: string, namespace: string, items: KeyedTranslation[]): Promise<SyncBatchResponse>;
|
|
223
|
+
/**
|
|
224
|
+
* A queued sync job's state. GET /api/integrations/sync/jobs/{id}.
|
|
225
|
+
*
|
|
226
|
+
* The lookup is project-scoped server-side, and finished non-DLQ jobs are
|
|
227
|
+
* purged after ~1h — an unknown id 404s, which is why the CLI also keeps its
|
|
228
|
+
* own history (a job that completed yesterday can only be answered from disk).
|
|
229
|
+
*/
|
|
230
|
+
getSyncJob(jobId: string): Promise<SyncJobStatus>;
|
|
231
|
+
/**
|
|
232
|
+
* Jobs the backend gave up on. GET /api/integrations/sync/dead-letter-queue.
|
|
233
|
+
* Needs the `ops:queue` scope (master keys have it; publishable `pk_` keys
|
|
234
|
+
* do not — those get a 403).
|
|
88
235
|
*/
|
|
89
|
-
|
|
236
|
+
getDeadLetterQueue(): Promise<SyncJobStatus[]>;
|
|
237
|
+
/** Queue-wide metrics. GET /api/integrations/sync/queue-health (`ops:queue`). */
|
|
238
|
+
getQueueHealth(): Promise<QueueHealth>;
|
|
90
239
|
/**
|
|
91
240
|
* Write target-locale translations as drafts (editor-visible, not public
|
|
92
241
|
* until mark-ready). PUT /api/integrations/targets/batch.
|
|
@@ -120,4 +269,5 @@ export declare class LionRapidApiClient {
|
|
|
120
269
|
*/
|
|
121
270
|
private doFetch;
|
|
122
271
|
}
|
|
272
|
+
export {};
|
|
123
273
|
//# sourceMappingURL=api-client.d.ts.map
|