@blocofy/cli 0.8.0 → 0.10.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
@@ -10,48 +10,362 @@ cd path/to/theme
10
10
  npx @blocofy/cli theme dev # http://localhost:3030 — local theme + live data, livereload
11
11
  ```
12
12
 
13
+ `login` saves the site under a named **context** (default: the site's slug) in
14
+ `~/.blocofy/credentials.json`. `blocofy link <dir> --context <name>` (or a pull into an empty
15
+ directory) then binds a project directory to that site so every command run inside it always
16
+ targets the right one — see [Contexts and project binding](#contexts-and-project-binding).
17
+
13
18
  ## Commands
14
19
 
15
- ```bash
16
- blocofy login [--url <url>] [--token <bcf_…>]
17
- # Save your platform URL + dev token (~/.blocofy/credentials.json, 0600).
18
- blocofy theme dev [dir] # Local dev server. Prints 3 views — local preview, a live-domain
19
- # preview link, and the theme editor — all auto-reloading on save.
20
- # --port <n> (default 3030), --no-sync (local preview only),
21
- # --name <name> (name the draft when first created)
22
- blocofy theme pull [dir] # Download the live theme to disk.
23
- blocofy theme push [dir] # Write the local theme to a DRAFT by default (create/update; no
24
- # delete) — preview & publish from the admin panel. --live writes
25
- # to the live site immediately (asks to confirm; add --yes for CI).
26
- # --name <name> names the new draft (draft mode only).
27
- # --prune also removes target files deleted locally (lists them
28
- # first; on the live theme asks to confirm — --yes for CI).
20
+ ### Login & contexts
21
+
22
+ ```
23
+ blocofy login [--context <name>] [--url <url>] [--token <bcf_…>] [--keychain]
24
+ ```
25
+ Verify a dev token against its site (`GET /api/dev/whoami`) and save it as a named context
26
+ (default name: the site's slug). Nothing is saved if verification fails. Get a token from the
27
+ admin panel → Settings → Theme CLI tokens.
28
+ - `--keychain` — keep the secret in the macOS keychain (or `BLOCOFY_SECRET_STORE=keychain`);
29
+ default is `~/.blocofy/secrets.json` (0600).
30
+
31
+ ```
32
+ blocofy login --api-key [--context <name>] [--api-url <url>]
33
+ ```
34
+ Verify a v1 API key (`blcf_live_…`, `GET /api/v1/ping`) and add it to a context. The key is
35
+ read from a **hidden prompt** — the flag takes no value, so it never lands in argv or shell
36
+ history. If the context already has a dev token for another site, nothing is saved
37
+ (`TARGET_CREDENTIAL_MISMATCH`).
38
+ - `--api-url <url>` — API origin (default `https://app.blocofy.com`).
39
+ - Non-interactive shells: set `BLOCOFY_API_KEY` + `BLOCOFY_API_URL` instead.
40
+
41
+ ```
42
+ blocofy contexts [--json] # list saved contexts (never prints secrets)
43
+ blocofy use <name> # default context for read-only commands outside a project
44
+ blocofy logout --context <name> # remove a context and its secrets
45
+ ```
46
+
47
+ ### Project binding
48
+
49
+ ```
50
+ blocofy link [dir] --context <name> [--adopt]
51
+ ```
52
+ Bind a project directory to the context's (verified) site: writes `.blocofy/project.json`
53
+ (commit it), `.blocofy/local.json` (your context; git-ignored) and `.blocofy/.gitignore`.
54
+ Refuses to rebind a directory bound to another site unless `--adopt`.
55
+
56
+ ```
57
+ blocofy target [dir] [--context <name>] [--json]
58
+ ```
59
+ Show which site a command in `[dir]` would hit (verified), without writing anything.
60
+
61
+ ### Theme
62
+
63
+ ```
64
+ blocofy theme dev [dir] [--port <n>] [--no-sync] [--name <name>]
65
+ ```
66
+ Start a dev server and print 3 auto-reloading views — Local, live-domain Preview, and the theme
67
+ Editor. Press `l` / `p` / `e` to open each, `q` to quit. Edit a file and save → every open view
68
+ reloads. Saves sync to a **draft** theme only (never the live site). `dir` defaults to cwd.
69
+ The target site is verified once at start; a long session keeps that target (restart it after
70
+ changing credentials, context or the project binding).
71
+ - `--port <n>` — local port (default 3030).
72
+ - `--no-sync` — local preview only (skip draft sync + remote views).
73
+ - `--name <name>` — name the draft when it is first created (ignored if it already exists).
74
+
75
+ ```
76
+ blocofy theme pull [dir] [--draft] [--instance <handle>]
77
+ ```
78
+ Download the live theme to disk. `dir` defaults to cwd.
79
+ - `--draft` — pull the draft theme (what `theme dev` syncs into) instead of live; creates the
80
+ draft if missing, so it needs a bound project.
81
+ - `--instance <handle>` — pull a specific theme by its handle (admin panel theme card, or
82
+ `blocofy status`).
83
+
84
+ ```
85
+ blocofy theme push [dir] [--live] [--yes] [--instance <handle>] [--prune]
86
+ [--name <name>] [--dry-run | --validate] [--diff]
87
+ [--idempotency-key <k>]
88
+ ```
89
+ By **default** writes to a draft theme (create/update; no delete) — preview & publish it from
90
+ the admin panel, never touching the live site. Publish it with `blocofy theme publish`.
91
+ - `--live` — write to the LIVE site immediately (no preview). Asks for confirmation first;
92
+ non-interactive shells must add `--yes`.
93
+ - `--draft` — explicit draft (same as the default; safe).
94
+ - `--yes` — confirm a `--live` push without prompting (CI/agents).
95
+ - `--instance <handle>` — push to a specific theme by its handle (safe targeted write — no
96
+ live-confirmation prompt).
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.
100
+ - `--diff` — show what a push would change vs the target (read-only), then stop.
101
+ - `--idempotency-key <k>` — attach a key so a retried push is not double-applied.
102
+ - `--prune` — also remove target files that no longer exist locally (`locales/` included);
103
+ lists them first, and on the live theme asks to confirm (non-interactive shells add `--yes`).
104
+
105
+ ```
29
106
  blocofy theme rename <handle> <new name>
30
- # Rename a theme (label only). Works on any theme, live included.
31
- # Handle from the panel theme card or `blocofy status`.
32
- blocofy login --api-key [--api-url <url>]
33
- # Save a v1 API key (blcf_live_…) for the `pages media-*` commands.
34
- # The flag takes NO value: the key is typed into a hidden prompt, so
35
- # it never lands in argv or shell history. Non-interactive shells use
36
- # BLOCOFY_API_KEY + BLOCOFY_API_URL instead (no prompt, nothing
37
- # written without them). --api-url defaults to https://app.blocofy.com.
107
+ ```
108
+ Rename a theme (the name is just a label). Works on any of your themes, including the live one.
109
+
110
+ ```
111
+ blocofy theme publish [--instance <handle>]
112
+ ```
113
+ Publish a draft theme to the LIVE site: it replaces the live theme for every visitor. With no
114
+ 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.
116
+ - `--instance <handle>` — publish a specific theme.
117
+
118
+ ### Status
119
+
120
+ ```
121
+ blocofy status
122
+ ```
123
+ Show the live theme, page distribution per instance, drafts, and a health flag (`ok` /
124
+ `live_instance_empty` / `pages_split`). For a problem it names the theme holding the pages, the
125
+ missing pages, why, and safe preview-first next steps — never a one-line fix.
126
+
127
+ ### Pages
128
+
129
+ ```
130
+ blocofy pages pull [dir] [--strict]
131
+ ```
132
+ Download published pages, one folder per language: `pages/<locale>/index.json` (home) and
133
+ `pages/<locale>/routes/<path>/index.json` (everything else). Files from the old flat layout
134
+ (`pages/<slug>.json`) are reported, never deleted or overwritten. If the site cannot export
135
+ every published page, nothing is written, every reason is printed
136
+ (`PAGES_EXPORT_INCOMPLETE`) and the exit code is 2 (`--strict`: warnings exit 1).
137
+
138
+ ```
139
+ blocofy pages push [dir] [--dry-run] [--strict] [--force --reason <text>]
140
+ ```
141
+ Write `pages/**.json` to the site. Updates **existing** pages only — never creates or deletes a
142
+ page; unchanged pages are skipped. Every file is checked first: an invalid file, two files
143
+ pointing at the same page, or a folder/locale mismatch changes **no** page. Every pulled file
144
+ carries `base_revision` (the page as you pulled it); the push first asks for a plan (per page:
145
+ action, live/draft, changed fields), prints it, then pushes exactly that plan.
146
+ - If a page changed on the site since you pulled it (`PAGES_REVISION_CONFLICT`) or a file has no
147
+ `base_revision` (`PAGES_BASE_REVISION_REQUIRED`), nothing is changed: run `blocofy pages pull`,
148
+ merge your edits, and push again.
149
+ - If the site changes between the plan and the push, nothing is changed either
150
+ (`PAGES_PLAN_STALE`) — run the push again.
151
+ - `--force --reason <text>` — overwrite anyway (reason: 1–500 characters); the forced pages are
152
+ listed. Against an older server that cannot check revisions, the push runs as before with a
153
+ warning.
154
+ - `--dry-run` — print the plan, write nothing.
155
+ - If publishing stops unexpectedly after the plan (`PAGES_APPLY_FAILED` or another publish
156
+ error), some pages may already be applied: the per-file result printed is authoritative, the
157
+ exit code is non-zero, and re-running the push is safe.
158
+
159
+ ```
160
+ blocofy pages check [dir] [--strict]
161
+ ```
162
+ Check page files. Offline: paths, JSON, layout, duplicates, missing `base_revision`
163
+ (`PAGES_BASE_REVISION_MISSING` warning). Logged in: also the site's languages and a server-side
164
+ dry run. Exit 1 on errors (`--strict`: warnings too).
165
+
166
+ ```
167
+ blocofy pages migrate-layout [dir] [--dry-run | --write] [--strict]
168
+ ```
169
+ Move old flat-layout files (`pages/<slug>.json`) to language folders. `--dry-run` (default)
170
+ prints the plan; `--write` moves only proven files. Any ambiguity or conflict: nothing is moved,
171
+ exit 1. Files without `"locale"` use the site's default language (needs login); with `--write`
172
+ outside a bound project only explicit `--context`/env credentials are used.
173
+
174
+ ```
38
175
  blocofy pages media-uses <page-handle> [--json]
39
- # List a page's localized-media decisions on its newest draft
40
- # (v1 API, pages:read) with the draft revision id/version.
176
+ ```
177
+ 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`.
179
+
180
+ ```
41
181
  blocofy pages media-decide <page-handle> --decisions <file.json>
42
- [--expected-revision-id <n> --expected-version <n>] [--json]
43
- # Apply the file's { "decisions": [...] } (max 20) to the draft
44
- # atomically (v1 API, pages:write). Without the --expected-* pair
45
- # the CLI GETs the draft first and uses its current revision;
46
- # items without idempotency_key get a random UUID. Exit 0 applied
47
- # (or "No changes" when already recorded), 1 usage/auth/network/5xx
48
- # (no retry), 2 server refusal (4xx) — {error} JSON on stderr.
182
+ [--expected-revision-id <n> --expected-version <n>] [--json]
183
+ ```
184
+ Apply one or more media decisions to the page's draft atomically (v1 API, `pages:write`). The
185
+ file is `{ "decisions": [ { path, facet, decision, target_asset?, alt?, caption?, decorative?,
186
+ idempotency_key?, witness? } ] }` (max 20). Without the two `--expected-*` flags the CLI first
187
+ `GET`s the draft and uses its current revision id/version; items without an `idempotency_key`
188
+ get a random UUID. Transient failures (429/502/503/504, network) are retried against the same
189
+ idempotency key, so a retry replays the same batch. Exit 0 on success (or "No changes" when
190
+ every item was already recorded).
191
+
192
+ ### Settings
193
+
194
+ ```
195
+ blocofy settings pull [dir]
196
+ blocofy settings push [dir] (--instance <handle> | --live [--yes])
197
+ ```
198
+ Download / upload `config/settings.json` (theme tokens/settings + color schemes). A push
199
+ **names its target** — there is no implicit live write, and the command refuses with neither
200
+ flag:
201
+ - `--instance <handle>` — write that theme's settings; a draft shows them in its preview and
202
+ goes live with `blocofy theme publish --instance <handle>`.
203
+ - `--live` — write the LIVE theme (asks to confirm; non-interactive shells add `--yes`).
204
+
205
+ After a push the CLI says where it applied (preview now / live now / after deploy).
206
+
207
+ ### Site (declarative state)
208
+
209
+ The `site` commands move a whole site — pages, navigation, theme source, settings, chrome,
210
+ translations, media — as one declarative tree on disk (`blocofy-site.json` + `site/**` +
211
+ `theme/**` + `pages/**` + `media/**`), instead of the per-scope `theme`/`pages`/`settings`
212
+ commands above.
213
+
214
+ ```
215
+ blocofy site export [dir]
216
+ ```
217
+ Download the whole site as a declarative tree into `[dir]`, plus every media file's bytes at
218
+ `media/files/<sha256>` (downloaded and hash-verified). Refuses to overwrite a directory bound to
219
+ another site (same binding rule as every other pull).
220
+
221
+ ```
222
+ blocofy site validate [dir]
223
+ ```
224
+ Check an exported (or hand-authored) tree **offline** — zero network requests: paths, the
225
+ path↔content binding (a page file's locale/slug must match its folder/name, and so on),
226
+ duplicate identities, the size/count limits, and the tree against its own manifest digest.
227
+ - `--strict` — warnings also exit non-zero.
228
+
229
+ ```
230
+ blocofy site migrate [dir] [--dry-run | --write]
231
+ ```
232
+ Turn a directory left by the **older separate** `theme pull` / `pages pull` / `settings pull`
233
+ into the site-state v1 tree layout (theme directories at the root → `theme/**`,
234
+ `config/settings.json` → `theme/config/settings.json`). Purely local: no network request, no
235
+ target/identity check, `.blocofy/` project binding untouched. `pages/**` files are never moved
236
+ (the location is already identical); a file still at the old flat page layout is left alone with
237
+ a note to run `blocofy pages migrate-layout` first.
238
+ - `--dry-run` (default) — print the plan: every move, every file left alone, every conflict.
239
+ - `--write` — perform it.
240
+ - Any ambiguity or conflict (a target already exists with different content, an unreadable or
241
+ symlinked entry, a path the shared site-state rules refuse): zero moves, exit 1.
242
+ - Never invents the parts a directory of separate pulls never had (`blocofy-site.json`,
243
+ `site/locales.json`, globals, navigation, translations, `theme/chrome/**`) — it says so and
244
+ points at `blocofy site export` for those.
245
+
246
+ ```
247
+ blocofy site plan [dir] [--target new|<handle>] [--mode same_site|restore]
248
+ [--accept-live-effects locales] [--json]
249
+ ```
250
+ Ask the site what applying this tree **would** do — no write. Needs **both** a dev token and a
251
+ v1 API key (the dev token is what a theme deploy uses later). Prints the status (`planned` /
252
+ `awaiting_assets` / `awaiting_theme_source` / `draft_complete`), the steps, and any missing
253
+ assets or a differing theme source.
254
+ - `--target new|<handle>` — a fresh draft theme version (default), or a specific one you own
255
+ (from a previous plan/apply's target instance).
256
+ - `--mode same_site|restore` — `same_site` refuses a page that changed on this site since the
257
+ tree was exported; `restore` (default) does not.
258
+ - `--accept-live-effects locales` — required before a state that adds a language may be applied
259
+ (publishing a language prepares its homepage LIVE).
260
+
261
+ ```
262
+ blocofy site apply [dir] [--target new|<handle>] [--mode same_site|restore]
263
+ [--accept-live-effects locales] [--json]
264
+ ```
265
+ Build the state into a **draft** theme version — the live site is never touched. Loops: plan →
266
+ (upload missing media via the v1 API, or deploy the theme source via the same canonical path
267
+ `theme push --instance` uses) → plan → apply, until the target reports `draft_complete` or 5
268
+ passes are used. Safe to re-run: every step is idempotent and a re-plan picks up exactly what is
269
+ left, so an apply interrupted by anything (network, Ctrl-C, a crash) resumes with the same
270
+ command. Preview it from the admin panel, then run `blocofy site publish`.
271
+
272
+ ```
273
+ blocofy site publish [dir] [--target new|<handle>] [--mode same_site|restore] [--yes]
274
+ ```
275
+ Make an applied state the LIVE site: pointer swap, then navigation, then settings. Refuses (exit
276
+ 2) if the state is not fully applied yet — run `site apply` first. Separate live gate: asks to
277
+ confirm (non-interactive shells must add `--yes`).
278
+
279
+ ```
49
280
  blocofy --version
50
281
  blocofy --help
51
282
  ```
52
283
 
284
+ ## Contexts and project binding
285
+
286
+ Every remote command verifies its site first and prints a `Target` block on stderr. The context
287
+ is chosen in this order:
288
+
289
+ 1. `--context <name>`
290
+ 2. `BLOCOFY_CONTEXT`
291
+ 3. env credentials (`BLOCOFY_URL` + `BLOCOFY_TOKEN` and/or `BLOCOFY_API_URL` + `BLOCOFY_API_KEY`)
292
+ 4. `.blocofy/local.json` (the project's own context choice)
293
+ 5. the one saved context matching the project's site
294
+ 6. (terminal) pick from the matches
295
+
296
+ 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).
301
+
302
+ A binding made against an older server has no recorded `platform_origin`: it still matches the
303
+ same site (one warning printed; run `blocofy link --adopt` to record it). A server that reports
304
+ no origin cannot serve a binding that records one — that refuses with `TARGET_UNVERIFIED` (the
305
+ server cannot prove it is the platform the binding was made against).
306
+
307
+ 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:
309
+ **nothing was read or written** — the command stops before it touches the site or the local
310
+ project files, and exits 3 (see [Exit codes](#exit-codes)).
311
+
312
+ `blocofy site migrate` is the one exception: it never resolves a target or a context at all (no
313
+ network, no identity call), so it leaves any `.blocofy/` binding in the directory completely
314
+ untouched.
315
+
316
+ ## Exit codes
317
+
318
+ | Code | Meaning |
319
+ | --- | --- |
320
+ | 0 | Ok. |
321
+ | 1 | Usage / network / HTTP 5xx / a local check that refuses before any request (e.g. a `site migrate` conflict, a `pages migrate-layout` ambiguity). |
322
+ | 2 | The server refused the request (HTTP 4xx) — its `{error}` JSON is printed. |
323
+ | 3 | Target/binding refusal — nothing was read or written (see [Contexts and project binding](#contexts-and-project-binding)). |
324
+ | 4 | `site apply` only: not finished within its bounded pass count. Every step already applied is safe — re-run the same command to resume. |
325
+
326
+ `--json`: every failure prints `{"error":{"code","message","details"}}` as the **last** stderr
327
+ line; the target block (`{"target":…}`) and any warning lines are printed on stderr before it.
328
+
329
+ Retries: network errors and HTTP 429/502/503/504 are retried up to 3 times (`Retry-After`
330
+ 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.
333
+
53
334
  ## Changelog
54
335
 
336
+ - **0.10.0** — Named contexts + verified project binding, and a declarative whole-site state.
337
+ - `login` now saves a **named context** (`--context <name>`, default the site's slug) instead
338
+ of one global credentials pair; `blocofy contexts` / `use` / `logout` manage them, and
339
+ `blocofy link [dir] --context <name> [--adopt]` binds a project directory to one verified
340
+ site (`.blocofy/project.json`, committed; `.blocofy/local.json`, git-ignored).
341
+ `blocofy target [dir]` shows which site a command would hit without writing anything.
342
+ `login --keychain` keeps the secret in the macOS keychain instead of
343
+ `~/.blocofy/secrets.json`. A pre-0.10 `credentials.json` is migrated on first use (backup:
344
+ `~/.blocofy/credentials.v1.bak.json`).
345
+ - New `blocofy site export/validate/migrate/plan/apply/publish`: pull a whole site as one
346
+ declarative tree, check it fully offline, turn an older separate-pulls directory into that
347
+ tree layout (`site migrate`), ask what applying it would do, build it into a draft theme
348
+ version, and make it live. `site apply` is resumable — every step is idempotent and picks up
349
+ exactly what is left.
350
+ - `pages push` speaks page revision CAS: every pulled file carries `base_revision`, a push
351
+ previews its plan before writing, and a page changed on the site since the pull (or missing
352
+ `base_revision`) refuses with nothing changed instead of silently overwriting — recover with
353
+ `pages pull` + push again, or `--force --reason <text>`.
354
+ - `settings push` now **requires** `--instance <handle>` or `--live` — there is no longer an
355
+ implicit live write.
356
+ - Consistent exit codes across every command (see [Exit codes](#exit-codes)) and a `--json`
357
+ envelope (`{"error":{code,message,details}}`) as the last stderr line on any failure.
358
+
359
+ - **0.9.0** — Locale-aware page files for multilingual sites (needs a platform with page file protocol 2;
360
+ against an older server page commands stop with `PAGES_SERVER_UPGRADE_REQUIRED` and write nothing).
361
+ `pages pull` writes `pages/<locale>/index.json` and `pages/<locale>/routes/<path>/index.json`, so the same
362
+ URL in two languages no longer shares one file. `pages push` preflights the whole directory and the server
363
+ refuses the batch before any write on an invalid file, duplicate target or folder/locale mismatch; a
364
+ failure after preflight prints the per-file result. New `pages check`, `pages push --dry-run` and
365
+ `pages migrate-layout [--dry-run | --write]`. Pull validates every server path, never follows symlinks,
366
+ never writes outside the target directory and stages its writes (0.8.0 could write a server-supplied
367
+ `../` path outside it). An incomplete export prints every reason with `PAGES_EXPORT_INCOMPLETE` and writes
368
+ nothing. Limits: 500 page files, 2 MiB per file, 4 MiB total.
55
369
  - **0.8.0** — Page media decisions over the public v1 API: `pages media-uses <page-handle>` lists a
56
370
  page's localized-media decisions on its newest draft, and `pages media-decide <page-handle>
57
371
  --decisions <file.json>` applies a batch of decisions to that draft atomically (all or nothing;
@@ -123,11 +437,13 @@ editor — without affecting your published theme. Save a file and **every open
123
437
  (the platform-rendered pages connect back to the local dev server's reload channel). Publish
124
438
  the draft from the theme editor when you're ready.
125
439
 
126
- Credentials come from `~/.blocofy/credentials.json` (written by `blocofy login`) or the
440
+ Credentials live in `~/.blocofy/credentials.json` (contexts, no secrets) + either
441
+ `~/.blocofy/secrets.json` (0600) or the macOS keychain — written by `blocofy login`, or the
127
442
  `BLOCOFY_URL` + `BLOCOFY_TOKEN` environment variables (for CI/automation). The v1 API commands
128
- (`pages media-uses`, `pages media-decide`) use a separate `blcf_live_…` key from the same file
129
- (written by `blocofy login --api-key`) or `BLOCOFY_API_KEY` + `BLOCOFY_API_URL` — both variables
130
- together; with only one set the CLI fails instead of falling back to the file.
443
+ (`pages media-uses`/`media-decide`, `site plan`/`export`/`publish`) use a separate
444
+ `blcf_live_…` key from the same file (written by `blocofy login --api-key`) or `BLOCOFY_API_KEY`
445
+ + `BLOCOFY_API_URL` — both variables together; with only one set the CLI fails instead of
446
+ falling back to the file.
131
447
 
132
448
  ## Theme structure
133
449
 
@@ -140,12 +456,17 @@ A theme is a directory of files grouped by top-level folder:
140
456
  | `block/` | Repeatable pieces used inside sections |
141
457
  | `partial/` | Shared snippets (header/footer, …) |
142
458
  | `asset/` | CSS and static files (`theme.css`) |
143
- | `pages/<slug>.json` | Page content (`/` → `pages/index.json`) |
144
- | `config/settings.json` | Theme settings + color schemes |
145
- | `config/settings_schema.json` | Theme settings panel schema (synced with `pull`/`push`) |
459
+ | `locales/` | Legacy Liquid translation files (`<tag>.json`, `<tag>.default.json`) |
460
+ | `pages/<locale>/index.json`, `pages/<locale>/routes/<path>/index.json` | Page content, one folder per language (`blocofy pages pull/push/check/migrate-layout`). The old `pages/<slug>.json` layout is still read, with a warning |
461
+ | `config/settings.json` | Theme settings + color schemes (`blocofy settings pull`; `settings push` needs `--instance <handle>` or `--live`) |
462
+ | `config/settings_schema.json` | Theme settings panel schema (synced with `theme pull`/`push`) |
146
463
 
147
464
  Liquid templates use the `.liquid` extension; files under `asset/` are served as-is.
148
465
 
466
+ `blocofy site export` pulls the same content plus navigation, chrome and translations as one
467
+ declarative tree instead (see [Site (declarative state)](#site-declarative-state)); `blocofy
468
+ site migrate` turns a directory built from the commands above into that tree's layout.
469
+
149
470
  ## Development
150
471
 
151
472
  Zero runtime dependencies (Node built-ins only). Run the tests with: