@blocofy/cli 0.10.0 → 0.11.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/README.md CHANGED
@@ -40,7 +40,7 @@ history. If the context already has a dev token for another site, nothing is sav
40
40
 
41
41
  ```
42
42
  blocofy contexts [--json] # list saved contexts (never prints secrets)
43
- blocofy use <name> # default context for read-only commands outside a project
43
+ blocofy use <name> # default context for status / target / pages check outside a project
44
44
  blocofy logout --context <name> # remove a context and its secrets
45
45
  ```
46
46
 
@@ -95,30 +95,66 @@ the admin panel, never touching the live site. Publish it with `blocofy theme pu
95
95
  - `--instance <handle>` — push to a specific theme by its handle (safe targeted write — no
96
96
  live-confirmation prompt).
97
97
  - `--name <name>` — name the new draft (draft mode only; ignored on `--live`/`--instance`).
98
- - `--dry-run` / `--validate` — validate on the server without writing (auth + snapshot + Liquid
99
- check); the two flags are aliases.
98
+ - `--dry-run` / `--validate` — check the push on the server without writing; the two flags are
99
+ aliases. The dry run covers exactly the files the push would send (remote-only files it keeps
100
+ included; with `--prune`, without the ones it would remove). A current platform plans the whole
101
+ deploy and prints the target, its pointer version and what happens to each file
102
+ (`+` created, `~` updated, `-` removed; unchanged files are counted).
100
103
  - `--diff` — show what a push would change vs the target (read-only), then stop.
101
104
  - `--idempotency-key <k>` — attach a key so a retried push is not double-applied.
102
105
  - `--prune` — also remove target files that no longer exist locally (`locales/` included);
103
106
  lists them first, and on the live theme asks to confirm (non-interactive shells add `--yes`).
104
107
 
105
- ```
106
- blocofy theme rename <handle> <new name>
108
+ Every push first runs that dry run, then writes the same files. On a current platform the write is
109
+ bound to the dry run: it is refused, with nothing written, if the files differ from what was checked
110
+ (`manifest_mismatch`), the target theme was deployed again in between (`pointer_version_conflict`),
111
+ or the push would now write to another theme than the one its dry run planned against — the live
112
+ theme was switched, or the draft to reuse changed (`target_changed`: nothing was deployed, though a
113
+ draft push may have created a new, empty draft that the next push reuses; run the push again).
114
+ A write that gets no answer (network error, 429/502/503/504) is resent (up to 3 times) under the same
115
+ key; a push the platform already committed is then reported as deployed (`already applied by an
116
+ earlier push with the same idempotency key`). If that resend is refused, the refusal cannot say
117
+ what the earlier attempt did: the message says whether it was committed or its outcome is
118
+ unknown (`details.earlierAttempt`), never "Nothing was written". Check the theme the dry run bound
119
+ the write to (`details.expectedTargetInstance`, not the current target a refusal names) with
120
+ `blocofy theme push --diff --instance <handle>` — or, for a new draft, find it with `blocofy status` —
121
+ before running the push again. A write whose last answer is still not definite (HTTP 500 or another
122
+ 5xx, a 503, a 502 `readback_unverified` with `outcomeUnknown`, or no answer) has an unknown outcome
123
+ (`details.outcome: "unknown"`): the message names the push's key (`details.idempotencyKey`); run the
124
+ same command again with `--idempotency-key <that key>`, and a push that was deployed is reported as
125
+ deployed instead of being written again.
126
+ If the dry run would remove a file the push did not carry, the push stops
127
+ (`THEME_PUSH_TARGET_CHANGED`). The message says which case each file is:
128
+ - a path the push cannot send (not a file inside a theme folder, e.g. a bare `layout` row; listed in
129
+ `details.notCarryable`) stops every run the same way, and only `--prune` gets past it (it removes
130
+ the file);
131
+ - any other file was either added while the push was running, and running the push again keeps it,
132
+ or the push cannot read it (for example a theme file that is not published): then it stops every
133
+ run the same way, and you add `--prune` to remove it or add a local file at that path to replace it.
134
+
135
+ `Deployed atomically` is printed only when the server read the written files back and they match;
136
+ an older platform that does not read back gets `Deployed: … not verified`.
137
+
138
+ ```
139
+ blocofy theme rename <handle> <new name> [--dir <dir>]
107
140
  ```
108
141
  Rename a theme (the name is just a label). Works on any of your themes, including the live one.
142
+ - `--dir <dir>` — the bound project whose site the theme is on (default: cwd).
109
143
 
110
144
  ```
111
- blocofy theme publish [--instance <handle>]
145
+ blocofy theme publish [dir] [--instance <handle>]
112
146
  ```
113
147
  Publish a draft theme to the LIVE site: it replaces the live theme for every visitor. With no
114
148
  flag, publishes the draft that `theme dev` / `theme push --draft` writes into. The server
115
- refuses to publish a theme that has no pages (it would 404); preview first.
149
+ refuses to publish a theme that has no pages (it would 404); preview first. The site is the one
150
+ `[dir]`'s project is bound to (`dir` defaults to cwd), so `blocofy theme push ./shop && blocofy
151
+ theme publish ./shop` always publishes `./shop`'s site, whatever directory you run it from.
116
152
  - `--instance <handle>` — publish a specific theme.
117
153
 
118
154
  ### Status
119
155
 
120
156
  ```
121
- blocofy status
157
+ blocofy status [dir]
122
158
  ```
123
159
  Show the live theme, page distribution per instance, drafts, and a health flag (`ok` /
124
160
  `live_instance_empty` / `pages_split`). For a problem it names the theme holding the pages, the
@@ -172,14 +208,15 @@ exit 1. Files without `"locale"` use the site's default language (needs login);
172
208
  outside a bound project only explicit `--context`/env credentials are used.
173
209
 
174
210
  ```
175
- blocofy pages media-uses <page-handle> [--json]
211
+ blocofy pages media-uses <page-handle> [--dir <dir>] [--json]
176
212
  ```
177
213
  List a page's localized-media decisions on its newest **draft** (v1 API, `pages:read`). Prints
178
- the draft's revision id/version needed by `media-decide`.
214
+ the draft's revision id/version needed by `media-decide`. `--dir <dir>` (both media commands):
215
+ the bound project whose site the page is on (default: cwd).
179
216
 
180
217
  ```
181
218
  blocofy pages media-decide <page-handle> --decisions <file.json>
182
- [--expected-revision-id <n> --expected-version <n>] [--json]
219
+ [--expected-revision-id <n> --expected-version <n>] [--dir <dir>] [--json]
183
220
  ```
184
221
  Apply one or more media decisions to the page's draft atomically (v1 API, `pages:write`). The
185
222
  file is `{ "decisions": [ { path, facet, decision, target_asset?, alt?, caption?, decorative?,
@@ -283,8 +320,28 @@ blocofy --help
283
320
 
284
321
  ## Contexts and project binding
285
322
 
286
- Every remote command verifies its site first and prints a `Target` block on stderr. The context
287
- is chosen in this order:
323
+ A **context** is one named operator profile for one site. It references two **separate**
324
+ credentials, each kept in the secret store and never printed: the theme dev token (`bcf_…`,
325
+ verified with `GET /api/dev/whoami`) and the v1 API key (`blcf_live_…`, verified with
326
+ `GET /api/v1/ping`). They stay two credentials with their own scopes — the CLI never merges them
327
+ into one token and never sends one to the other's endpoint. Both pairs of a context must resolve
328
+ to the same site (`TARGET_CREDENTIAL_MISMATCH`); a secret of the wrong type in either slot is
329
+ refused before any request (`TARGET_CREDENTIAL_WRONG_TYPE`, exit 1 — e.g. a `blcf_live_…` key in
330
+ `BLOCOFY_TOKEN`).
331
+
332
+ Every remote command verifies its site first and prints a `Target` block on stderr (`--json`:
333
+ the same as a `{"target":…}` line with `platform_origin`, `context_source`,
334
+ `context_overrides`, `command` and `mode`):
335
+
336
+ ```
337
+ Target: Alpha Bakery · s1a2b3 · alpha.myblocofy.com
338
+ Platform: https://app.blocofy.com
339
+ Context: alpha (from .blocofy/local.json)
340
+ Binding: .blocofy/project.json
341
+ Operation: theme push · draft
342
+ ```
343
+
344
+ The context is chosen in this order:
288
345
 
289
346
  1. `--context <name>`
290
347
  2. `BLOCOFY_CONTEXT`
@@ -292,12 +349,32 @@ is chosen in this order:
292
349
  4. `.blocofy/local.json` (the project's own context choice)
293
350
  5. the one saved context matching the project's site
294
351
  6. (terminal) pick from the matches
352
+ 7. outside a project, and only for `status`, `target` and `pages check`: the `blocofy use` default
353
+
354
+ **Conflicts fail closed.** `BLOCOFY_CONTEXT`, the env credentials and `.blocofy/local.json` are
355
+ each a choice. When two of them name different contexts, the command is refused before
356
+ anything is read or written (`TARGET_CONTEXT_CONFLICT`, exit 3) — e.g. `BLOCOFY_URL`/
357
+ `BLOCOFY_TOKEN` exported in a shell that then runs inside a project whose `local.json` names a
358
+ context. Settle it with `--context <name>` (or unset the others); the `Context` line then lists
359
+ what it overrode, e.g. `alpha (from --context; overrides env credentials)`.
295
360
 
296
361
  Inside a **bound project**, `blocofy use` is ignored — the project's binding decides, not the
297
- global default context. Commands that change a site (`theme push`/`publish`/`rename`, `theme
298
- dev` sync, `pages push`, `settings push`, `pages media-decide`) need a bound project; a pull into
299
- a new empty directory binds it automatically. A wrong project/site pairing changes nothing
300
- (`TARGET_SITE_MISMATCH` — pass `--adopt` on `link` to rebind deliberately).
362
+ global default context. Outside one, the `use` default serves only `status`, `target` and
363
+ `pages check`, and the `Context` line says so (`from default context (blocofy use)`); every other
364
+ command in an unbound directory needs `--context`, `BLOCOFY_CONTEXT` or env credentials, so a
365
+ `use` in another terminal never changes what a diff, dry run or plan compares against. Commands
366
+ that change a site (`theme push`/`publish`/`rename`, `theme dev` sync, `pages push`, `settings
367
+ push`, `pages media-decide`) need a bound project; a pull into a new empty directory binds it
368
+ automatically. A wrong project/site pairing changes nothing (`TARGET_SITE_MISMATCH` — pass
369
+ `--adopt` on `link` to rebind deliberately).
370
+
371
+ Every command resolves its binding from the directory it acts on — the `[dir]` argument (or
372
+ `--dir <dir>` for `theme rename` and `pages media-uses|media-decide`), else cwd.
373
+
374
+ **CI:** commit `.blocofy/project.json` (never `local.json`) and set the env pairs the job needs
375
+ (`BLOCOFY_URL` + `BLOCOFY_TOKEN` for theme/pages/settings, plus `BLOCOFY_API_URL` +
376
+ `BLOCOFY_API_KEY` for the v1 commands and `site plan`/`apply`). The env credentials are then the
377
+ only choice, and each command still verifies they belong to the committed binding's site.
301
378
 
302
379
  A binding made against an older server has no recorded `platform_origin`: it still matches the
303
380
  same site (one warning printed; run `blocofy link --adopt` to record it). A server that reports
@@ -305,7 +382,8 @@ no origin cannot serve a binding that records one — that refuses with `TARGET_
305
382
  server cannot prove it is the platform the binding was made against).
306
383
 
307
384
  A refusal in this area (`TARGET_SITE_MISMATCH`, `TARGET_UNVERIFIED`, `TARGET_CREDENTIAL_MISMATCH`,
308
- `TARGET_CONTEXT_REQUIRED`, `TARGET_CONTEXT_UNKNOWN`, `TARGET_BINDING_INVALID`) always means:
385
+ `TARGET_CONTEXT_CONFLICT`, `TARGET_CONTEXT_REQUIRED`, `TARGET_CONTEXT_UNKNOWN`,
386
+ `TARGET_BINDING_INVALID`, and `TARGET_CREDENTIAL_WRONG_TYPE` with exit 1) always means:
309
387
  **nothing was read or written** — the command stops before it touches the site or the local
310
388
  project files, and exits 3 (see [Exit codes](#exit-codes)).
311
389
 
@@ -328,11 +406,68 @@ line; the target block (`{"target":…}`) and any warning lines are printed on s
328
406
 
329
407
  Retries: network errors and HTTP 429/502/503/504 are retried up to 3 times (`Retry-After`
330
408
  honoured, max 30s per wait; else 0.3s/0.9s/2s), resending the identical request (`pages push`
331
- carries one `x-idempotency-key` per push). HTTP 500 is never retried. Each retry prints a notice
332
- on stderr.
409
+ carries one `x-idempotency-key` per push). HTTP 500 is never retried, nor is a `theme push` dry
410
+ run answered 502 `readback_unverified` (a verdict on a rolled-back plan, not a transient failure).
411
+ Each retry prints a notice on stderr.
333
412
 
334
413
  ## Changelog
335
414
 
415
+ - **0.11.0** — One named context per site, one explicit target (customer item 1.8).
416
+ The dev token and the v1 API key remain two separate credentials; a context references both.
417
+ - **Breaking:** conflicting context choices now fail closed. When `BLOCOFY_CONTEXT`, the env
418
+ credentials (`BLOCOFY_URL`/`BLOCOFY_TOKEN`, `BLOCOFY_API_URL`/`BLOCOFY_API_KEY`) and a
419
+ project's `.blocofy/local.json` name different contexts, the command stops with
420
+ `TARGET_CONTEXT_CONFLICT` (exit 3) instead of silently taking the first. A shell that exports
421
+ the env credentials inside a project with a `local.json` must pass `--context <name>` (or
422
+ unset them). An explicit `--context` still wins, and the target block lists what it overrode.
423
+ - **Breaking:** outside a bound project the `blocofy use` default is used only by `status`,
424
+ `target` and `pages check` (as contract C2 specified). `theme push --diff/--dry-run`,
425
+ `pages push --dry-run`, `site plan`, `theme dev --no-sync` and `pages media-uses` in an unbound
426
+ directory now need `--context`, `BLOCOFY_CONTEXT` or env credentials
427
+ (`TARGET_CONTEXT_REQUIRED`).
428
+ - The target block shows the platform the site was verified on and where the context choice
429
+ came from (`Platform:` line; `Context: <name> (from …)`); `--json` adds `platform_origin`,
430
+ `context_source`, `context_overrides`, `command` and `mode`.
431
+ - `theme publish [dir]` and `status [dir]` take the project directory as an argument, and
432
+ `theme rename` / `pages media-uses|media-decide` take `--dir <dir>`. Before, they always used
433
+ cwd, so `theme push ./a && theme publish` run from another project published that project's
434
+ draft. A directory that does not exist, or one given where the command takes none (a second
435
+ positional to `target`/`status`/`theme publish`, any directory positional to
436
+ `pages media-uses|media-decide`), is a usage error (exit 1) before any request.
437
+ - A secret of the wrong type (a `blcf_live_…` key as the dev token, a `bcf_…` token as the API
438
+ key) is refused before any request with `TARGET_CREDENTIAL_WRONG_TYPE`, naming the variable
439
+ or context — it was sent to the wrong endpoint and reported as `TARGET_UNVERIFIED`.
440
+ `login --token blcf_…` points at `login --api-key`.
441
+ - Theme push preflight bound to the write (customer item 6.5):
442
+ - `theme push` checks the MERGED payload (local files plus the remote-only files it keeps) in its
443
+ dry run, then writes exactly those files with the dry run's `manifestHash` and
444
+ `expectedPointerVersion`, and — when the platform names it — the theme the dry run planned
445
+ against (`expectedTargetInstance`; refused with `target_changed` if the target moved). The dry
446
+ run carries its own throwaway idempotency key, so
447
+ `--idempotency-key` retries of a committed push still converge.
448
+ - A write resent after an attempt without an answer is reported as deployed when the platform
449
+ had committed it. A refusal of such a resend says the earlier attempt's outcome is unknown (or
450
+ that it was committed) instead of "Nothing was written", and `details.earlierAttempt` names it.
451
+ - A write that gets no definite answer (a 5xx, a 503, `readback_unverified` with an unknown
452
+ outcome, or no answer after the resends) is reported as an unknown outcome, not a failure: the
453
+ message names this push's key, to run the same command again with `--idempotency-key <key>`
454
+ (the platform then reports the committed deploy instead of writing it again); `--json` details
455
+ carry `outcome: "unknown"` and `idempotencyKey`.
456
+ - A `target_changed` refusal names the theme the dry run planned against and the current target,
457
+ and says "Nothing was deployed" (a `--draft` push may have created a new, empty draft, which
458
+ the next push reuses). After a write's `readback_unverified` the CLI points at
459
+ `theme push --diff --instance <handle>` for the theme the write was bound to (or `blocofy status`
460
+ for a new draft), not at a plain `--diff`, which compares with the live theme only.
461
+ - Per-file outcomes are printed for the push and for `--dry-run`. `Deployed atomically` appears only
462
+ with a verified readback. New refusals with messages: `pointer_version_conflict`,
463
+ `site_state_version_conflict`, `manifest_mismatch`, `readback_unverified` (worded for the dry
464
+ run, where nothing was written, or for the write; `details.phase` says which) and the preflight path
465
+ errors (`path_too_long`, `reserved_path`, `binary_content_rejected`…, naming the file).
466
+ - A push stops with `THEME_PUSH_TARGET_CHANGED` when the dry run would remove a file the push did
467
+ not carry; the message says whether a re-run can keep it or only `--prune` gets past it, and
468
+ `details.notCarryable` lists the paths the push cannot send. `--prune` lists them for
469
+ confirmation instead. `--dry-run --prune` now plans the pruned set.
470
+ - Against an older platform the write body is unchanged from 0.10.
336
471
  - **0.10.0** — Named contexts + verified project binding, and a declarative whole-site state.
337
472
  - `login` now saves a **named context** (`--context <name>`, default the site's slug) instead
338
473
  of one global credentials pair; `blocofy contexts` / `use` / `logout` manage them, and