@lionrapid/cli 0.3.0 → 0.5.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 +128 -2
- package/dist/api-client.d.ts +79 -1
- package/dist/api-client.js +60 -1
- package/dist/cli.js +294 -45
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +47 -5
- package/dist/commands/init.d.ts +73 -2
- package/dist/commands/init.js +0 -0
- package/dist/commands/login.d.ts +50 -4
- package/dist/commands/login.js +70 -16
- package/dist/commands/pull.d.ts +15 -1
- package/dist/commands/pull.js +92 -4
- package/dist/commands/push.d.ts +62 -1
- package/dist/commands/push.js +198 -5
- package/dist/commands/status.d.ts +15 -0
- package/dist/commands/status.js +16 -5
- package/dist/commands/whoami.d.ts +50 -0
- package/dist/commands/whoami.js +66 -0
- package/dist/config.d.ts +100 -5
- package/dist/config.js +239 -22
- package/dist/history.d.ts +44 -0
- package/dist/history.js +65 -0
- package/dist/job-history.d.ts +11 -2
- package/dist/job-history.js +1 -13
- package/dist/lockfile.d.ts +90 -0
- package/dist/lockfile.js +110 -0
- package/dist/time.d.ts +27 -0
- package/dist/time.js +53 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,134 @@
|
|
|
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
|
-
##
|
|
6
|
+
## 0.5.0
|
|
7
|
+
|
|
8
|
+
### Changed — behaviour
|
|
9
|
+
|
|
10
|
+
- **`pushed` now reports what the server wrote, not what went on the wire.** In
|
|
11
|
+
target mode it is the server's own `created + updated`. The same push can
|
|
12
|
+
therefore report a smaller number than before — that number was previously
|
|
13
|
+
describing the CLI's outbox. Source mode is unchanged: a `202` can only
|
|
14
|
+
honestly claim "queued".
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **The server can decline to overwrite your translators, and the CLI now says
|
|
19
|
+
so.** A push from a master or publishable key no longer overwrites a row a
|
|
20
|
+
human committed — the server skips it and reports which ones back. `push`
|
|
21
|
+
names the key, the reason, and the wording the server kept:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
✓ pushed 0 target string(s) across 1 file(s)
|
|
25
|
+
· locales/es/home.json: 0 (1 not written)
|
|
26
|
+
⚠ 1 row(s) were NOT overwritten — the server is protecting them:
|
|
27
|
+
es:home:hero.title human_committed (server has: "Envia mas rapido")
|
|
28
|
+
A human committed these. There is no CLI override — change them in the
|
|
29
|
+
Visual Editor, or use "Use for all" to apply one wording across the project.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
There is deliberately **no flag to force past this**. The paths to a
|
|
33
|
+
deliberate overwrite are the Visual Editor and "Use for all", both of which
|
|
34
|
+
put a person in the loop. A protected row is also never marked ready.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **`denied` and `denied_rows` have never been visible to anyone.** The client
|
|
39
|
+
discarded the `targets/batch` response body entirely (`Promise<void>`), the
|
|
40
|
+
same way `syncBatch` did before 0.3.0 — so rows the server refused on
|
|
41
|
+
authorization grounds were reported by the server and shown to nobody, for as
|
|
42
|
+
long as the field has existed. The body is now returned and normalised, with a
|
|
43
|
+
`TARGET_BATCH_RESPONSE_FIELDS` drift guard beside the sync one so a third
|
|
44
|
+
dropped field is a typecheck failure.
|
|
45
|
+
- **A row the server simply had nothing to do with is no longer called
|
|
46
|
+
"not written".** An unchanged row lands in no server bucket, and the per-file
|
|
47
|
+
figure was computed as `sent − written`, so a no-op read as a refusal — the
|
|
48
|
+
output claimed a problem the user could not find, because the row it implied
|
|
49
|
+
was refused never appeared in the list. Now derived from the server's own
|
|
50
|
+
buckets.
|
|
51
|
+
- **`push --force` no longer claims to overwrite server values.** It forces past
|
|
52
|
+
the local lockfile diff; it does not and cannot override server-side
|
|
53
|
+
protection. The help text says so.
|
|
54
|
+
- **A target whose source row does not exist is no longer reported as pushed.**
|
|
55
|
+
The server writes nothing and answers `skipped`; the CLI said
|
|
56
|
+
`pushed 1 target string(s)`.
|
|
57
|
+
|
|
58
|
+
## 0.4.0
|
|
59
|
+
|
|
60
|
+
### Changed — behaviour, read this before upgrading
|
|
61
|
+
|
|
62
|
+
- **`push` is now incremental.** It diffs your sources against a new
|
|
63
|
+
`.lionrapid/lock.json` and sends only what changed; a second run of an
|
|
64
|
+
unchanged project pushes nothing. **Commit the lockfile** — like
|
|
65
|
+
`package-lock.json` it is the shared baseline, and a per-machine one would
|
|
66
|
+
make CI and a laptop disagree about what changed.
|
|
67
|
+
|
|
68
|
+
Recovery matters here: a batch the server accepts with `202` is recorded as
|
|
69
|
+
sent, and nothing waits for the job. If that job later dead-letters, a plain
|
|
70
|
+
re-push **skips** those rows. Use `push --backfill-missing` (sends exactly
|
|
71
|
+
what the server is missing) or `push --force` (resends everything).
|
|
72
|
+
|
|
73
|
+
- **`LIONRAPID_API_KEY` is now held to the same trusted-host rule as a saved
|
|
74
|
+
default key.** A `server:` that is not `*.lionrapid.com` or localhost will
|
|
75
|
+
refuse to receive it, because CI is precisely where a real secret meets a
|
|
76
|
+
`lionrapid.yml` that arrives by pull request. Self-hosted or staging: set
|
|
77
|
+
`LIONRAPID_ALLOW_UNTRUSTED_HOST=1` (accepted only as `1`/`true`). This
|
|
78
|
+
knowingly reverses the earlier position that CI sets the variable
|
|
79
|
+
deliberately — in CI the operator and the `server:` value are set by
|
|
80
|
+
different people, so exporting a key is not the same as approving its
|
|
81
|
+
destination.
|
|
82
|
+
|
|
83
|
+
- **`DEFAULT_SERVER` is `https://app.lionrapid.com`.** It was
|
|
84
|
+
`api.lionrapid.com`, which has no DNS record.
|
|
85
|
+
|
|
86
|
+
- **`targetLocales` is optional.** A config carrying only a source stanza is
|
|
87
|
+
valid; `push` uploads sources and warns that `pull` has nothing to do.
|
|
88
|
+
Previously `init` wrote a config its own schema rejected.
|
|
89
|
+
|
|
90
|
+
- **`project:` (free text) is removed.** The API key selects the project and
|
|
91
|
+
nothing in the config does; the field was a label nobody checked. A config
|
|
92
|
+
still carrying one parses and ignores it.
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
|
|
96
|
+
- **`lionrapid init` is one pass** — detect, resolve the server, prompt/verify
|
|
97
|
+
and **save** the key, fetch locales, capture `projectId`, write the config.
|
|
98
|
+
No second `login`, no second `init`, no hand-editing.
|
|
99
|
+
- **`lionrapid whoami`** — which key is about to be used and where it is going.
|
|
100
|
+
Offline, masked to the last four characters, names an env override explicitly.
|
|
101
|
+
- **A project-mismatch guard.** `push` refuses _before sending a single row_
|
|
102
|
+
when the config's `projectId` and the key's project disagree, naming both —
|
|
103
|
+
the fix depends on which one is wrong.
|
|
104
|
+
- **`pull` no longer clobbers local edits.** An edit newer than the server is
|
|
105
|
+
named and kept; `--force` takes the server's version and snapshots yours to
|
|
106
|
+
`.lionrapid/history/` first (gitignored — a local safety copy, not a team
|
|
107
|
+
fact). `push --force` does the same in the other direction.
|
|
108
|
+
- **`push --dry-run`** reports the diff: new, changed, unchanged.
|
|
109
|
+
- **`logout`** removes the only scope when there is one, offers a picker when
|
|
110
|
+
there are several, refuses non-interactively, and `--all` clears everything.
|
|
111
|
+
- **Key-capability guidance** in `doctor`, `whoami` and `push` — a `pk_` cannot
|
|
112
|
+
write or publish targets, said before you hit the 403.
|
|
113
|
+
- **`status --refresh`**, which never downgrades a terminal verdict: a
|
|
114
|
+
`completed` job the server has since purged still reads `completed`.
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
|
|
118
|
+
- `doctor` blamed a missing API key when the config was invalid; it now says
|
|
119
|
+
**skipped** and prints the config's actual problems.
|
|
120
|
+
- The no-key error named neither the server it wanted nor the scopes it had.
|
|
121
|
+
- `status` showed jobs finishing **before** they were pushed — offset-less
|
|
122
|
+
server timestamps were parsed as local time against a UTC clock.
|
|
123
|
+
- The credentials path ignored `process.env.HOME`, so the test suite read the
|
|
124
|
+
real `~/.lionrapid/credentials`.
|
|
125
|
+
- `validate` reported "all source keys translated" against **zero** target
|
|
126
|
+
locales.
|
|
127
|
+
- Four NUL bytes in `init.ts` made the file invisible to `grep`, for everyone,
|
|
128
|
+
permanently, because they were committed. A repo-wide guard test now fails on
|
|
129
|
+
any recurrence. Note `file(1)` does **not** detect this.
|
|
130
|
+
- `types check` is proven to run offline; a lint that needs credentials is a
|
|
131
|
+
lint nobody runs in CI.
|
|
132
|
+
|
|
133
|
+
## 0.3.0 — 2026-08-06
|
|
7
134
|
|
|
8
135
|
### Added
|
|
9
136
|
|
|
@@ -13,7 +140,6 @@ All notable changes to `@lionrapid/cli`. Format based on
|
|
|
13
140
|
`void`). A push whose backend job later failed or dead-lettered was therefore
|
|
14
141
|
indistinguishable from one that worked: `pushed N string(s)` described the
|
|
15
142
|
CLI's own outbox and nothing else. Now:
|
|
16
|
-
|
|
17
143
|
- `syncBatch` returns the server's `BatchSyncJobResponse`, and `push` collects
|
|
18
144
|
every `job_ids` entry (not just the legacy `job_id`, so multi-locale batches
|
|
19
145
|
stay observable);
|
package/dist/api-client.d.ts
CHANGED
|
@@ -63,6 +63,62 @@ type NoMissingContractFields<T extends never> = T;
|
|
|
63
63
|
* no lint suppression, and it is checked because `tsc --noEmit` covers `src/`.
|
|
64
64
|
*/
|
|
65
65
|
export type SyncBatchResponseCoversContract = NoMissingContractFields<Exclude<(typeof BATCH_SYNC_RESPONSE_FIELDS)[number], keyof SyncBatchResponse>>;
|
|
66
|
+
/** One row the server declined to write, and why. Shared by denied/protected. */
|
|
67
|
+
export interface TargetRowOutcome {
|
|
68
|
+
/** Composite key of the refused row (`locale:namespace:key`). */
|
|
69
|
+
key: string;
|
|
70
|
+
/** `human_committed`, `locked`, `forbidden` — the server's own word. */
|
|
71
|
+
reason: string;
|
|
72
|
+
/** What the server currently holds, so a caller can show what survived. */
|
|
73
|
+
current_text?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The body of `PUT /api/integrations/targets/batch` — the server's
|
|
77
|
+
* `TargetBatchUpdateResponse`. Wire names, like {@link SyncBatchResponse},
|
|
78
|
+
* because this type IS the contract.
|
|
79
|
+
*
|
|
80
|
+
* THE SERVER REFUSES ROWS AND SAYS WHICH. `denied` (per-row authorization) has
|
|
81
|
+
* been reported for as long as it has existed and the CLI never showed it,
|
|
82
|
+
* because `targetsBatch` returned `void` — the same discard `syncBatch` had
|
|
83
|
+
* before 0.3.0. `protected` (tm-platform#162) joins it: a MACHINE push no
|
|
84
|
+
* longer overwrites a human-committed row, and there is deliberately no
|
|
85
|
+
* override flag.
|
|
86
|
+
*
|
|
87
|
+
* `protected` is NOT `skipped`. `skipped` means the source row was not found
|
|
88
|
+
* or the value would not parse — a different thing, and both can be non-zero
|
|
89
|
+
* in one push.
|
|
90
|
+
*/
|
|
91
|
+
export interface TargetBatchResponse {
|
|
92
|
+
status: string;
|
|
93
|
+
/** New target rows written. */
|
|
94
|
+
created: number;
|
|
95
|
+
/** Existing target rows overwritten. */
|
|
96
|
+
updated: number;
|
|
97
|
+
/** Source row missing, or the value would not parse. NOT a refusal. */
|
|
98
|
+
skipped: number;
|
|
99
|
+
/** Rows the caller was not authorized to edit. */
|
|
100
|
+
denied: number;
|
|
101
|
+
denied_rows: TargetRowOutcome[];
|
|
102
|
+
/** Human-committed rows a machine caller must not overwrite. */
|
|
103
|
+
protected: number;
|
|
104
|
+
protected_rows: TargetRowOutcome[];
|
|
105
|
+
/** Rows refused by the directive-parity gate. */
|
|
106
|
+
rejected: number;
|
|
107
|
+
/** Items processed — includes everything above. */
|
|
108
|
+
total: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Fields of `TargetBatchUpdateResponse` the CLI must carry.
|
|
112
|
+
*
|
|
113
|
+
* Same drift guard as {@link BATCH_SYNC_RESPONSE_FIELDS}, and for the same
|
|
114
|
+
* reason twice over: this is the SECOND response body the CLI silently
|
|
115
|
+
* dropped. `denied_rows` was reported by the server and invisible to users for
|
|
116
|
+
* its whole life. A field added here without a matching one on
|
|
117
|
+
* {@link TargetBatchResponse} is a `tsc --noEmit` failure.
|
|
118
|
+
*/
|
|
119
|
+
export declare const TARGET_BATCH_RESPONSE_FIELDS: readonly ["status", "created", "updated", "skipped", "denied", "denied_rows", "protected", "protected_rows", "rejected", "total"];
|
|
120
|
+
/** Compiles only when every listed field exists on {@link TargetBatchResponse}. */
|
|
121
|
+
export type TargetBatchResponseCoversContract = NoMissingContractFields<Exclude<(typeof TARGET_BATCH_RESPONSE_FIELDS)[number], keyof TargetBatchResponse>>;
|
|
66
122
|
/**
|
|
67
123
|
* A sync job's server-side state — `GET /api/integrations/sync/jobs/{id}`
|
|
68
124
|
* (`SyncJobStatusResponse`). Camel-cased because, unlike
|
|
@@ -105,6 +161,16 @@ export interface NamespaceBundle {
|
|
|
105
161
|
notModified: boolean;
|
|
106
162
|
/** True on a 423 — the namespace exists but nothing is approved for delivery yet. */
|
|
107
163
|
locked: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* The project this key resolves to, from `meta.project_uuid`.
|
|
166
|
+
*
|
|
167
|
+
* REAL BUT UNDECLARED: the route sets `response_model=TranslationResponse`
|
|
168
|
+
* yet returns a raw Response so it can attach the ETag header, which bypasses
|
|
169
|
+
* FastAPI model filtering — so the field ships without being in the contract
|
|
170
|
+
* (server task #70). Anyone tidying that route to return the declared model
|
|
171
|
+
* removes it, in a change that reads as cosmetic. Treat as optional forever.
|
|
172
|
+
*/
|
|
173
|
+
projectId?: string;
|
|
108
174
|
}
|
|
109
175
|
export type FetchImpl = (input: string, init?: RequestInit) => Promise<Response>;
|
|
110
176
|
export interface ApiClientOptions {
|
|
@@ -183,6 +249,18 @@ export declare function normalizeTranslations(raw: unknown): Record<string, Cont
|
|
|
183
249
|
* — you simply get no job ids to report, which the CLI says out loud.
|
|
184
250
|
*/
|
|
185
251
|
export declare function normalizeSyncBatchResponse(raw: unknown): SyncBatchResponse;
|
|
252
|
+
/**
|
|
253
|
+
* Project the targets/batch body onto {@link TargetBatchResponse}, field by
|
|
254
|
+
* field.
|
|
255
|
+
*
|
|
256
|
+
* Every field degrades to zero/empty rather than throwing. That is not
|
|
257
|
+
* defensive habit: the deployment this was written against does NOT serve
|
|
258
|
+
* `protected`/`protected_rows` yet — its OpenAPI lists status, created,
|
|
259
|
+
* updated, skipped, denied, denied_rows, rejected, rejected_keys, total — so
|
|
260
|
+
* "absent" is the CURRENT reality for most servers, not a theoretical older
|
|
261
|
+
* one. An absent refusal must read as "nothing was refused", never as a crash.
|
|
262
|
+
*/
|
|
263
|
+
export declare function normalizeTargetBatchResponse(raw: unknown): TargetBatchResponse;
|
|
186
264
|
/** Project a `SyncJobStatusResponse` (job status, or a DLQ entry) onto {@link SyncJobStatus}. */
|
|
187
265
|
export declare function normalizeSyncJob(raw: unknown): SyncJobStatus;
|
|
188
266
|
export declare class LionRapidApiClient {
|
|
@@ -240,7 +318,7 @@ export declare class LionRapidApiClient {
|
|
|
240
318
|
* Write target-locale translations as drafts (editor-visible, not public
|
|
241
319
|
* until mark-ready). PUT /api/integrations/targets/batch.
|
|
242
320
|
*/
|
|
243
|
-
targetsBatch(targetLocale: string, namespace: string, items: KeyedTranslation[]): Promise<
|
|
321
|
+
targetsBatch(targetLocale: string, namespace: string, items: KeyedTranslation[]): Promise<TargetBatchResponse>;
|
|
244
322
|
/**
|
|
245
323
|
* Publish a single target row. POST /api/integrations/targets/{key}/mark-ready.
|
|
246
324
|
* Requires an editor session token (the integration key is not authorized).
|
package/dist/api-client.js
CHANGED
|
@@ -30,6 +30,27 @@ export const BATCH_SYNC_RESPONSE_FIELDS = [
|
|
|
30
30
|
'total_keys',
|
|
31
31
|
'message',
|
|
32
32
|
];
|
|
33
|
+
/**
|
|
34
|
+
* Fields of `TargetBatchUpdateResponse` the CLI must carry.
|
|
35
|
+
*
|
|
36
|
+
* Same drift guard as {@link BATCH_SYNC_RESPONSE_FIELDS}, and for the same
|
|
37
|
+
* reason twice over: this is the SECOND response body the CLI silently
|
|
38
|
+
* dropped. `denied_rows` was reported by the server and invisible to users for
|
|
39
|
+
* its whole life. A field added here without a matching one on
|
|
40
|
+
* {@link TargetBatchResponse} is a `tsc --noEmit` failure.
|
|
41
|
+
*/
|
|
42
|
+
export const TARGET_BATCH_RESPONSE_FIELDS = [
|
|
43
|
+
'status',
|
|
44
|
+
'created',
|
|
45
|
+
'updated',
|
|
46
|
+
'skipped',
|
|
47
|
+
'denied',
|
|
48
|
+
'denied_rows',
|
|
49
|
+
'protected',
|
|
50
|
+
'protected_rows',
|
|
51
|
+
'rejected',
|
|
52
|
+
'total',
|
|
53
|
+
];
|
|
33
54
|
/** Default number of retries for transient failures. */
|
|
34
55
|
export const DEFAULT_RETRIES = 3;
|
|
35
56
|
/** Default per-attempt request timeout. */
|
|
@@ -213,6 +234,40 @@ export function normalizeSyncBatchResponse(raw) {
|
|
|
213
234
|
message: asString(body['message']) ?? '',
|
|
214
235
|
};
|
|
215
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Project the targets/batch body onto {@link TargetBatchResponse}, field by
|
|
239
|
+
* field.
|
|
240
|
+
*
|
|
241
|
+
* Every field degrades to zero/empty rather than throwing. That is not
|
|
242
|
+
* defensive habit: the deployment this was written against does NOT serve
|
|
243
|
+
* `protected`/`protected_rows` yet — its OpenAPI lists status, created,
|
|
244
|
+
* updated, skipped, denied, denied_rows, rejected, rejected_keys, total — so
|
|
245
|
+
* "absent" is the CURRENT reality for most servers, not a theoretical older
|
|
246
|
+
* one. An absent refusal must read as "nothing was refused", never as a crash.
|
|
247
|
+
*/
|
|
248
|
+
export function normalizeTargetBatchResponse(raw) {
|
|
249
|
+
const body = isRecord(raw) ? raw : {};
|
|
250
|
+
const count = (key) => typeof body[key] === 'number' && Number.isFinite(body[key]) ? body[key] : 0;
|
|
251
|
+
const rows = (key) => (Array.isArray(body[key]) ? body[key] : [])
|
|
252
|
+
.filter(isRecord)
|
|
253
|
+
.map((row) => ({
|
|
254
|
+
key: asString(row['key']) ?? '',
|
|
255
|
+
reason: asString(row['reason']) ?? 'unspecified',
|
|
256
|
+
...(asString(row['current_text']) ? { current_text: row['current_text'] } : {}),
|
|
257
|
+
}));
|
|
258
|
+
return {
|
|
259
|
+
status: asString(body['status']) ?? '',
|
|
260
|
+
created: count('created'),
|
|
261
|
+
updated: count('updated'),
|
|
262
|
+
skipped: count('skipped'),
|
|
263
|
+
denied: count('denied'),
|
|
264
|
+
denied_rows: rows('denied_rows'),
|
|
265
|
+
protected: count('protected'),
|
|
266
|
+
protected_rows: rows('protected_rows'),
|
|
267
|
+
rejected: count('rejected'),
|
|
268
|
+
total: count('total'),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
216
271
|
/** Project a `SyncJobStatusResponse` (job status, or a DLQ entry) onto {@link SyncJobStatus}. */
|
|
217
272
|
export function normalizeSyncJob(raw) {
|
|
218
273
|
const body = isRecord(raw) ? raw : {};
|
|
@@ -343,10 +398,11 @@ export class LionRapidApiClient {
|
|
|
343
398
|
key: compositeKey(targetLocale, namespace, it.key),
|
|
344
399
|
value: it.value,
|
|
345
400
|
}));
|
|
346
|
-
await this.request('PUT', '/api/integrations/targets/batch', {
|
|
401
|
+
const res = await this.request('PUT', '/api/integrations/targets/batch', {
|
|
347
402
|
translations,
|
|
348
403
|
format: 'contentunit',
|
|
349
404
|
});
|
|
405
|
+
return normalizeTargetBatchResponse(res);
|
|
350
406
|
}
|
|
351
407
|
/**
|
|
352
408
|
* Publish a single target row. POST /api/integrations/targets/{key}/mark-ready.
|
|
@@ -386,6 +442,9 @@ export class LionRapidApiClient {
|
|
|
386
442
|
return {
|
|
387
443
|
translations: normalizeTranslations(data.translations),
|
|
388
444
|
version: data.meta?.version != null ? String(data.meta.version) : undefined,
|
|
445
|
+
...(typeof data.meta?.project_uuid === 'string' && data.meta.project_uuid
|
|
446
|
+
? { projectId: data.meta.project_uuid }
|
|
447
|
+
: {}),
|
|
389
448
|
notModified: false,
|
|
390
449
|
locked: false,
|
|
391
450
|
};
|