@lionrapid/cli 0.4.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 +52 -0
- package/dist/api-client.d.ts +69 -1
- package/dist/api-client.js +57 -1
- package/dist/cli.js +22 -2
- package/dist/commands/push.d.ts +23 -1
- package/dist/commands/push.js +47 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,58 @@
|
|
|
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
|
+
## 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
|
+
|
|
6
58
|
## 0.4.0
|
|
7
59
|
|
|
8
60
|
### Changed — behaviour, read this before upgrading
|
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
|
|
@@ -193,6 +249,18 @@ export declare function normalizeTranslations(raw: unknown): Record<string, Cont
|
|
|
193
249
|
* — you simply get no job ids to report, which the CLI says out loud.
|
|
194
250
|
*/
|
|
195
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;
|
|
196
264
|
/** Project a `SyncJobStatusResponse` (job status, or a DLQ entry) onto {@link SyncJobStatus}. */
|
|
197
265
|
export declare function normalizeSyncJob(raw: unknown): SyncJobStatus;
|
|
198
266
|
export declare class LionRapidApiClient {
|
|
@@ -250,7 +318,7 @@ export declare class LionRapidApiClient {
|
|
|
250
318
|
* Write target-locale translations as drafts (editor-visible, not public
|
|
251
319
|
* until mark-ready). PUT /api/integrations/targets/batch.
|
|
252
320
|
*/
|
|
253
|
-
targetsBatch(targetLocale: string, namespace: string, items: KeyedTranslation[]): Promise<
|
|
321
|
+
targetsBatch(targetLocale: string, namespace: string, items: KeyedTranslation[]): Promise<TargetBatchResponse>;
|
|
254
322
|
/**
|
|
255
323
|
* Publish a single target row. POST /api/integrations/targets/{key}/mark-ready.
|
|
256
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.
|
package/dist/cli.js
CHANGED
|
@@ -602,7 +602,7 @@ export function buildProgram() {
|
|
|
602
602
|
.option('--publish', 'in --target mode, mark pushed rows ready (public)')
|
|
603
603
|
.option('--dry-run', 'parse + count what would be pushed, but make no network calls')
|
|
604
604
|
.option('--backfill-missing', 'send only the rows the server is missing (asks the server)')
|
|
605
|
-
.option('--force', '
|
|
605
|
+
.option('--force', 'ignore the lockfile and resend every row (saves overwritten server rows to .lionrapid/history/). Does NOT override server-side protection of human-committed rows')
|
|
606
606
|
.option('--json', 'emit the result as JSON on stdout (prose to stderr)')
|
|
607
607
|
.option('-c, --config <path>', 'path to lionrapid.yml')
|
|
608
608
|
.action(async (options) => {
|
|
@@ -658,7 +658,8 @@ export function buildProgram() {
|
|
|
658
658
|
continue;
|
|
659
659
|
}
|
|
660
660
|
const same = f.unchanged ? ` (${f.unchanged} unchanged)` : '';
|
|
661
|
-
|
|
661
|
+
const held = f.refused ? ` (${f.refused} not written)` : '';
|
|
662
|
+
out(` ${f.skipped ? '· skipped (missing)' : '·'} ${f.file}: ${f.count}${held}${same}`);
|
|
662
663
|
}
|
|
663
664
|
const failed = res.files.filter((f) => f.error);
|
|
664
665
|
if (failed.length > 0) {
|
|
@@ -669,6 +670,25 @@ export function buildProgram() {
|
|
|
669
670
|
out('⚠ uploaded as drafts but not published — publishing needs an editor session token.\n' +
|
|
670
671
|
' Set LIONRAPID_EDITOR_TOKEN, or publish in the dashboard.');
|
|
671
672
|
}
|
|
673
|
+
// Rows the server declined to overwrite. Same voice as pull's
|
|
674
|
+
// kept-local warning: name the key and the reason, because a bare
|
|
675
|
+
// count tells you something happened without telling you what.
|
|
676
|
+
const refused = [
|
|
677
|
+
...res.protected.map((r) => ({ ...r, kind: 'protected' })),
|
|
678
|
+
...res.denied.map((r) => ({ ...r, kind: 'denied' })),
|
|
679
|
+
];
|
|
680
|
+
if (refused.length > 0) {
|
|
681
|
+
out(`⚠ ${refused.length} row(s) were NOT overwritten — the server is protecting them:`);
|
|
682
|
+
const width = Math.max(...refused.map((r) => r.key.length));
|
|
683
|
+
for (const r of refused) {
|
|
684
|
+
const current = r.current_text ? ` (server has: "${r.current_text}")` : '';
|
|
685
|
+
out(` ${r.key.padEnd(width)} ${r.reason}${current}`);
|
|
686
|
+
}
|
|
687
|
+
// There is deliberately no override flag, so do not imply one.
|
|
688
|
+
// These are the paths that DO change a committed row.
|
|
689
|
+
out(' A human committed these. There is no CLI override — change them in the');
|
|
690
|
+
out(' Visual Editor, or use "Use for all" to apply one wording across the project.');
|
|
691
|
+
}
|
|
672
692
|
// The whole point: "pushed" above is what we SENT. The server stores
|
|
673
693
|
// it in the background, so print what to watch and where it is saved.
|
|
674
694
|
if (res.jobs.length > 0) {
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* via `targets/batch`, optionally publishing them with `--publish`.
|
|
15
15
|
*/
|
|
16
16
|
import { type LionRapidConfig } from '../config.js';
|
|
17
|
-
import type { LionRapidApiClient } from '../api-client.js';
|
|
17
|
+
import type { LionRapidApiClient, TargetRowOutcome } from '../api-client.js';
|
|
18
18
|
/**
|
|
19
19
|
* A sync job the server queued for one batch of this push.
|
|
20
20
|
*
|
|
@@ -72,6 +72,11 @@ export interface PushFileResult {
|
|
|
72
72
|
skipped?: boolean;
|
|
73
73
|
/** Keys this file already had on the server, per the lock — not re-sent. */
|
|
74
74
|
unchanged?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Rows sent for this file that the server did not write — protected,
|
|
77
|
+
* denied, or skipped. Target mode only; `count` is what actually landed.
|
|
78
|
+
*/
|
|
79
|
+
refused?: number;
|
|
75
80
|
/** Set when this file failed (parse or upload) — it was not (fully) pushed. */
|
|
76
81
|
error?: string;
|
|
77
82
|
}
|
|
@@ -84,6 +89,23 @@ export interface PushResult {
|
|
|
84
89
|
pushed: number;
|
|
85
90
|
/** Keys the lock says the server already has, so they were not sent again. */
|
|
86
91
|
skipped: number;
|
|
92
|
+
/**
|
|
93
|
+
* Rows the server REFUSED to overwrite because a human committed them.
|
|
94
|
+
*
|
|
95
|
+
* A machine caller (no user identity) does not get to overwrite a
|
|
96
|
+
* translator's committed work, and there is deliberately no override. These
|
|
97
|
+
* were NOT written, so they are not in `pushed`.
|
|
98
|
+
*/
|
|
99
|
+
protected: TargetRowOutcome[];
|
|
100
|
+
/** Rows the caller was not authorized to edit. Same class: not written. */
|
|
101
|
+
denied: TargetRowOutcome[];
|
|
102
|
+
/**
|
|
103
|
+
* Rows the SERVER skipped — source row missing, or the value would not
|
|
104
|
+
* parse. Distinct from `protected`: a different cause, and both can be
|
|
105
|
+
* non-zero in one push. Named to avoid colliding with `skipped` above,
|
|
106
|
+
* which is this CLI's lockfile diff.
|
|
107
|
+
*/
|
|
108
|
+
serverSkipped: number;
|
|
87
109
|
/**
|
|
88
110
|
* What this push is doing, split so `--dry-run` can answer the question you
|
|
89
111
|
* ran it to ask: am I ADDING strings, or overwriting ones that already
|
package/dist/commands/push.js
CHANGED
|
@@ -86,6 +86,21 @@ export async function runPush(config, client, opts) {
|
|
|
86
86
|
let skipped = 0;
|
|
87
87
|
let diffAdded = 0;
|
|
88
88
|
let diffChanged = 0;
|
|
89
|
+
const protectedRows = [];
|
|
90
|
+
const deniedRows = [];
|
|
91
|
+
let serverSkipped = 0;
|
|
92
|
+
/** Rows the server confirmed it wrote (target mode only). */
|
|
93
|
+
let written = 0;
|
|
94
|
+
/** The same, for the file currently being processed. */
|
|
95
|
+
let writtenForFile = 0;
|
|
96
|
+
/**
|
|
97
|
+
* Rows the server said it REFUSED for the current file, summed from its own
|
|
98
|
+
* buckets. Deliberately not (sent − written): a row whose text already
|
|
99
|
+
* matches is written by nobody and refused by nobody — the server leaves it
|
|
100
|
+
* out of every bucket and out of `total` — so the subtraction would report a
|
|
101
|
+
* no-op as a problem. Seen live: 2 sent, `protected: 1`, `total: 1`.
|
|
102
|
+
*/
|
|
103
|
+
let refusedForFile = 0;
|
|
89
104
|
// INCREMENTAL. The lock says what the server already has, so a second run
|
|
90
105
|
// sends "4 changed" rather than all 653. No lock (first run, or a fresh
|
|
91
106
|
// checkout) means we know nothing, which is a backfill — send everything.
|
|
@@ -138,6 +153,10 @@ export async function runPush(config, client, opts) {
|
|
|
138
153
|
const parseLocale = mode === 'source' ? config.sourceLocale : opts.target;
|
|
139
154
|
const entries = adapter.parse(readFileSync(absPath, 'utf8'), { locale: parseLocale });
|
|
140
155
|
const all = entries.map((e) => ({ key: e.key, value: e.value }));
|
|
156
|
+
// Reset per file: the header total is the sum of these, so they have to
|
|
157
|
+
// measure the same thing.
|
|
158
|
+
writtenForFile = 0;
|
|
159
|
+
refusedForFile = 0;
|
|
141
160
|
const hashes = new Map(all.map((it) => [it.key, hashSource(it.value)]));
|
|
142
161
|
// Three modes. `--force` and `--backfill-missing` both ASK THE SERVER
|
|
143
162
|
// rather than trusting the lock, which is what makes them a recovery
|
|
@@ -207,9 +226,25 @@ export async function runPush(config, client, opts) {
|
|
|
207
226
|
}
|
|
208
227
|
}
|
|
209
228
|
else {
|
|
210
|
-
await client.targetsBatch(opts.target, namespace, batch);
|
|
229
|
+
const outcome = await client.targetsBatch(opts.target, namespace, batch);
|
|
230
|
+
protectedRows.push(...outcome.protected_rows);
|
|
231
|
+
deniedRows.push(...outcome.denied_rows);
|
|
232
|
+
serverSkipped += outcome.skipped;
|
|
233
|
+
// Count what the SERVER says it wrote, not what we put on the
|
|
234
|
+
// wire. A refused row is not a pushed row — the whole point of
|
|
235
|
+
// this response is that "sent" and "stored" can differ.
|
|
236
|
+
written += outcome.created + outcome.updated;
|
|
237
|
+
writtenForFile += outcome.created + outcome.updated;
|
|
238
|
+
refusedForFile +=
|
|
239
|
+
outcome.protected + outcome.denied + outcome.skipped + outcome.rejected;
|
|
211
240
|
if (canPublish) {
|
|
241
|
+
// Never mark-ready a row the server declined to write: it would
|
|
242
|
+
// publish whatever is already there under the guise of this
|
|
243
|
+
// push, which is worse than the refusal it followed.
|
|
244
|
+
const refused = new Set([...outcome.protected_rows, ...outcome.denied_rows].map((r) => r.key));
|
|
212
245
|
for (const it of batch) {
|
|
246
|
+
if (refused.has(`${opts.target}:${namespace}:${it.key}`))
|
|
247
|
+
continue;
|
|
213
248
|
await client.markReady(opts.target, namespace, it.key);
|
|
214
249
|
}
|
|
215
250
|
}
|
|
@@ -225,10 +260,13 @@ export async function runPush(config, client, opts) {
|
|
|
225
260
|
lock.entries[key] = { ...lock.entries[key], sourceHash: hashes.get(it.key) };
|
|
226
261
|
}
|
|
227
262
|
}
|
|
228
|
-
pushed += items.length;
|
|
263
|
+
pushed += mode === 'target' ? 0 : items.length;
|
|
229
264
|
files.push({
|
|
230
265
|
file: relPath,
|
|
231
|
-
|
|
266
|
+
// Target mode reports what the SERVER wrote; source mode reports what
|
|
267
|
+
// was queued, which is all a 202 can honestly claim.
|
|
268
|
+
count: mode === 'target' ? writtenForFile : items.length,
|
|
269
|
+
...(mode === 'target' && refusedForFile > 0 ? { refused: refusedForFile } : {}),
|
|
232
270
|
...(all.length > items.length ? { unchanged: all.length - items.length } : {}),
|
|
233
271
|
});
|
|
234
272
|
}
|
|
@@ -254,8 +292,13 @@ export async function runPush(config, client, opts) {
|
|
|
254
292
|
writeLock(opts.lockPath, lock);
|
|
255
293
|
return {
|
|
256
294
|
mode,
|
|
257
|
-
|
|
295
|
+
// Target mode reports what the server confirmed it WROTE. Source mode
|
|
296
|
+
// reports what was queued, which is all a 202 can honestly claim.
|
|
297
|
+
pushed: mode === 'target' ? written : pushed,
|
|
258
298
|
skipped,
|
|
299
|
+
protected: protectedRows,
|
|
300
|
+
denied: deniedRows,
|
|
301
|
+
serverSkipped,
|
|
259
302
|
...(projectCheck ? { projectCheck } : {}),
|
|
260
303
|
...(mode === 'source'
|
|
261
304
|
? { diff: { added: diffAdded, changed: diffChanged, unchanged: skipped } }
|
package/package.json
CHANGED