@blocofy/cli 0.9.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 +483 -52
- package/bin/blocofy.mjs +1348 -276
- package/lib/args.mjs +2 -1
- package/lib/content-sync.mjs +129 -30
- package/lib/credentials.mjs +216 -60
- package/lib/http.mjs +63 -0
- package/lib/local-theme.mjs +7 -2
- package/lib/media-uses.mjs +27 -16
- package/lib/messages.mjs +42 -2
- package/lib/output.mjs +72 -0
- package/lib/page-files.mjs +12 -2
- package/lib/secret-store.mjs +133 -0
- package/lib/site-migrate.mjs +132 -0
- package/lib/site-state-client.mjs +92 -0
- package/lib/site-state-fs.mjs +137 -0
- package/lib/site-state.mjs +705 -0
- package/lib/target.mjs +485 -0
- package/lib/theme-sync.mjs +272 -139
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,63 +10,487 @@ 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
blocofy
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
blocofy
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 status / target / pages check 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` — 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).
|
|
103
|
+
- `--diff` — show what a push would change vs the target (read-only), then stop.
|
|
104
|
+
- `--idempotency-key <k>` — attach a key so a retried push is not double-applied.
|
|
105
|
+
- `--prune` — also remove target files that no longer exist locally (`locales/` included);
|
|
106
|
+
lists them first, and on the live theme asks to confirm (non-interactive shells add `--yes`).
|
|
107
|
+
|
|
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>]
|
|
140
|
+
```
|
|
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).
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
blocofy theme publish [dir] [--instance <handle>]
|
|
146
|
+
```
|
|
147
|
+
Publish a draft theme to the LIVE site: it replaces the live theme for every visitor. With no
|
|
148
|
+
flag, publishes the draft that `theme dev` / `theme push --draft` writes into. The server
|
|
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.
|
|
152
|
+
- `--instance <handle>` — publish a specific theme.
|
|
153
|
+
|
|
154
|
+
### Status
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
blocofy status [dir]
|
|
158
|
+
```
|
|
159
|
+
Show the live theme, page distribution per instance, drafts, and a health flag (`ok` /
|
|
160
|
+
`live_instance_empty` / `pages_split`). For a problem it names the theme holding the pages, the
|
|
161
|
+
missing pages, why, and safe preview-first next steps — never a one-line fix.
|
|
162
|
+
|
|
163
|
+
### Pages
|
|
164
|
+
|
|
165
|
+
```
|
|
49
166
|
blocofy pages pull [dir] [--strict]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
167
|
+
```
|
|
168
|
+
Download published pages, one folder per language: `pages/<locale>/index.json` (home) and
|
|
169
|
+
`pages/<locale>/routes/<path>/index.json` (everything else). Files from the old flat layout
|
|
170
|
+
(`pages/<slug>.json`) are reported, never deleted or overwritten. If the site cannot export
|
|
171
|
+
every published page, nothing is written, every reason is printed
|
|
172
|
+
(`PAGES_EXPORT_INCOMPLETE`) and the exit code is 2 (`--strict`: warnings exit 1).
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
blocofy pages push [dir] [--dry-run] [--strict] [--force --reason <text>]
|
|
176
|
+
```
|
|
177
|
+
Write `pages/**.json` to the site. Updates **existing** pages only — never creates or deletes a
|
|
178
|
+
page; unchanged pages are skipped. Every file is checked first: an invalid file, two files
|
|
179
|
+
pointing at the same page, or a folder/locale mismatch changes **no** page. Every pulled file
|
|
180
|
+
carries `base_revision` (the page as you pulled it); the push first asks for a plan (per page:
|
|
181
|
+
action, live/draft, changed fields), prints it, then pushes exactly that plan.
|
|
182
|
+
- If a page changed on the site since you pulled it (`PAGES_REVISION_CONFLICT`) or a file has no
|
|
183
|
+
`base_revision` (`PAGES_BASE_REVISION_REQUIRED`), nothing is changed: run `blocofy pages pull`,
|
|
184
|
+
merge your edits, and push again.
|
|
185
|
+
- If the site changes between the plan and the push, nothing is changed either
|
|
186
|
+
(`PAGES_PLAN_STALE`) — run the push again.
|
|
187
|
+
- `--force --reason <text>` — overwrite anyway (reason: 1–500 characters); the forced pages are
|
|
188
|
+
listed. Against an older server that cannot check revisions, the push runs as before with a
|
|
189
|
+
warning.
|
|
190
|
+
- `--dry-run` — print the plan, write nothing.
|
|
191
|
+
- If publishing stops unexpectedly after the plan (`PAGES_APPLY_FAILED` or another publish
|
|
192
|
+
error), some pages may already be applied: the per-file result printed is authoritative, the
|
|
193
|
+
exit code is non-zero, and re-running the push is safe.
|
|
194
|
+
|
|
195
|
+
```
|
|
59
196
|
blocofy pages check [dir] [--strict]
|
|
60
|
-
|
|
197
|
+
```
|
|
198
|
+
Check page files. Offline: paths, JSON, layout, duplicates, missing `base_revision`
|
|
199
|
+
(`PAGES_BASE_REVISION_MISSING` warning). Logged in: also the site's languages and a server-side
|
|
200
|
+
dry run. Exit 1 on errors (`--strict`: warnings too).
|
|
201
|
+
|
|
202
|
+
```
|
|
61
203
|
blocofy pages migrate-layout [dir] [--dry-run | --write] [--strict]
|
|
62
|
-
|
|
63
|
-
|
|
204
|
+
```
|
|
205
|
+
Move old flat-layout files (`pages/<slug>.json`) to language folders. `--dry-run` (default)
|
|
206
|
+
prints the plan; `--write` moves only proven files. Any ambiguity or conflict: nothing is moved,
|
|
207
|
+
exit 1. Files without `"locale"` use the site's default language (needs login); with `--write`
|
|
208
|
+
outside a bound project only explicit `--context`/env credentials are used.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
blocofy pages media-uses <page-handle> [--dir <dir>] [--json]
|
|
212
|
+
```
|
|
213
|
+
List a page's localized-media decisions on its newest **draft** (v1 API, `pages:read`). Prints
|
|
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).
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
blocofy pages media-decide <page-handle> --decisions <file.json>
|
|
219
|
+
[--expected-revision-id <n> --expected-version <n>] [--dir <dir>] [--json]
|
|
220
|
+
```
|
|
221
|
+
Apply one or more media decisions to the page's draft atomically (v1 API, `pages:write`). The
|
|
222
|
+
file is `{ "decisions": [ { path, facet, decision, target_asset?, alt?, caption?, decorative?,
|
|
223
|
+
idempotency_key?, witness? } ] }` (max 20). Without the two `--expected-*` flags the CLI first
|
|
224
|
+
`GET`s the draft and uses its current revision id/version; items without an `idempotency_key`
|
|
225
|
+
get a random UUID. Transient failures (429/502/503/504, network) are retried against the same
|
|
226
|
+
idempotency key, so a retry replays the same batch. Exit 0 on success (or "No changes" when
|
|
227
|
+
every item was already recorded).
|
|
228
|
+
|
|
229
|
+
### Settings
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
blocofy settings pull [dir]
|
|
233
|
+
blocofy settings push [dir] (--instance <handle> | --live [--yes])
|
|
234
|
+
```
|
|
235
|
+
Download / upload `config/settings.json` (theme tokens/settings + color schemes). A push
|
|
236
|
+
**names its target** — there is no implicit live write, and the command refuses with neither
|
|
237
|
+
flag:
|
|
238
|
+
- `--instance <handle>` — write that theme's settings; a draft shows them in its preview and
|
|
239
|
+
goes live with `blocofy theme publish --instance <handle>`.
|
|
240
|
+
- `--live` — write the LIVE theme (asks to confirm; non-interactive shells add `--yes`).
|
|
241
|
+
|
|
242
|
+
After a push the CLI says where it applied (preview now / live now / after deploy).
|
|
243
|
+
|
|
244
|
+
### Site (declarative state)
|
|
245
|
+
|
|
246
|
+
The `site` commands move a whole site — pages, navigation, theme source, settings, chrome,
|
|
247
|
+
translations, media — as one declarative tree on disk (`blocofy-site.json` + `site/**` +
|
|
248
|
+
`theme/**` + `pages/**` + `media/**`), instead of the per-scope `theme`/`pages`/`settings`
|
|
249
|
+
commands above.
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
blocofy site export [dir]
|
|
253
|
+
```
|
|
254
|
+
Download the whole site as a declarative tree into `[dir]`, plus every media file's bytes at
|
|
255
|
+
`media/files/<sha256>` (downloaded and hash-verified). Refuses to overwrite a directory bound to
|
|
256
|
+
another site (same binding rule as every other pull).
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
blocofy site validate [dir]
|
|
260
|
+
```
|
|
261
|
+
Check an exported (or hand-authored) tree **offline** — zero network requests: paths, the
|
|
262
|
+
path↔content binding (a page file's locale/slug must match its folder/name, and so on),
|
|
263
|
+
duplicate identities, the size/count limits, and the tree against its own manifest digest.
|
|
264
|
+
- `--strict` — warnings also exit non-zero.
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
blocofy site migrate [dir] [--dry-run | --write]
|
|
268
|
+
```
|
|
269
|
+
Turn a directory left by the **older separate** `theme pull` / `pages pull` / `settings pull`
|
|
270
|
+
into the site-state v1 tree layout (theme directories at the root → `theme/**`,
|
|
271
|
+
`config/settings.json` → `theme/config/settings.json`). Purely local: no network request, no
|
|
272
|
+
target/identity check, `.blocofy/` project binding untouched. `pages/**` files are never moved
|
|
273
|
+
(the location is already identical); a file still at the old flat page layout is left alone with
|
|
274
|
+
a note to run `blocofy pages migrate-layout` first.
|
|
275
|
+
- `--dry-run` (default) — print the plan: every move, every file left alone, every conflict.
|
|
276
|
+
- `--write` — perform it.
|
|
277
|
+
- Any ambiguity or conflict (a target already exists with different content, an unreadable or
|
|
278
|
+
symlinked entry, a path the shared site-state rules refuse): zero moves, exit 1.
|
|
279
|
+
- Never invents the parts a directory of separate pulls never had (`blocofy-site.json`,
|
|
280
|
+
`site/locales.json`, globals, navigation, translations, `theme/chrome/**`) — it says so and
|
|
281
|
+
points at `blocofy site export` for those.
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
blocofy site plan [dir] [--target new|<handle>] [--mode same_site|restore]
|
|
285
|
+
[--accept-live-effects locales] [--json]
|
|
286
|
+
```
|
|
287
|
+
Ask the site what applying this tree **would** do — no write. Needs **both** a dev token and a
|
|
288
|
+
v1 API key (the dev token is what a theme deploy uses later). Prints the status (`planned` /
|
|
289
|
+
`awaiting_assets` / `awaiting_theme_source` / `draft_complete`), the steps, and any missing
|
|
290
|
+
assets or a differing theme source.
|
|
291
|
+
- `--target new|<handle>` — a fresh draft theme version (default), or a specific one you own
|
|
292
|
+
(from a previous plan/apply's target instance).
|
|
293
|
+
- `--mode same_site|restore` — `same_site` refuses a page that changed on this site since the
|
|
294
|
+
tree was exported; `restore` (default) does not.
|
|
295
|
+
- `--accept-live-effects locales` — required before a state that adds a language may be applied
|
|
296
|
+
(publishing a language prepares its homepage LIVE).
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
blocofy site apply [dir] [--target new|<handle>] [--mode same_site|restore]
|
|
300
|
+
[--accept-live-effects locales] [--json]
|
|
301
|
+
```
|
|
302
|
+
Build the state into a **draft** theme version — the live site is never touched. Loops: plan →
|
|
303
|
+
(upload missing media via the v1 API, or deploy the theme source via the same canonical path
|
|
304
|
+
`theme push --instance` uses) → plan → apply, until the target reports `draft_complete` or 5
|
|
305
|
+
passes are used. Safe to re-run: every step is idempotent and a re-plan picks up exactly what is
|
|
306
|
+
left, so an apply interrupted by anything (network, Ctrl-C, a crash) resumes with the same
|
|
307
|
+
command. Preview it from the admin panel, then run `blocofy site publish`.
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
blocofy site publish [dir] [--target new|<handle>] [--mode same_site|restore] [--yes]
|
|
311
|
+
```
|
|
312
|
+
Make an applied state the LIVE site: pointer swap, then navigation, then settings. Refuses (exit
|
|
313
|
+
2) if the state is not fully applied yet — run `site apply` first. Separate live gate: asks to
|
|
314
|
+
confirm (non-interactive shells must add `--yes`).
|
|
315
|
+
|
|
316
|
+
```
|
|
64
317
|
blocofy --version
|
|
65
318
|
blocofy --help
|
|
66
319
|
```
|
|
67
320
|
|
|
321
|
+
## Contexts and project binding
|
|
322
|
+
|
|
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:
|
|
345
|
+
|
|
346
|
+
1. `--context <name>`
|
|
347
|
+
2. `BLOCOFY_CONTEXT`
|
|
348
|
+
3. env credentials (`BLOCOFY_URL` + `BLOCOFY_TOKEN` and/or `BLOCOFY_API_URL` + `BLOCOFY_API_KEY`)
|
|
349
|
+
4. `.blocofy/local.json` (the project's own context choice)
|
|
350
|
+
5. the one saved context matching the project's site
|
|
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)`.
|
|
360
|
+
|
|
361
|
+
Inside a **bound project**, `blocofy use` is ignored — the project's binding decides, not the
|
|
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.
|
|
378
|
+
|
|
379
|
+
A binding made against an older server has no recorded `platform_origin`: it still matches the
|
|
380
|
+
same site (one warning printed; run `blocofy link --adopt` to record it). A server that reports
|
|
381
|
+
no origin cannot serve a binding that records one — that refuses with `TARGET_UNVERIFIED` (the
|
|
382
|
+
server cannot prove it is the platform the binding was made against).
|
|
383
|
+
|
|
384
|
+
A refusal in this area (`TARGET_SITE_MISMATCH`, `TARGET_UNVERIFIED`, `TARGET_CREDENTIAL_MISMATCH`,
|
|
385
|
+
`TARGET_CONTEXT_CONFLICT`, `TARGET_CONTEXT_REQUIRED`, `TARGET_CONTEXT_UNKNOWN`,
|
|
386
|
+
`TARGET_BINDING_INVALID`, and `TARGET_CREDENTIAL_WRONG_TYPE` with exit 1) always means:
|
|
387
|
+
**nothing was read or written** — the command stops before it touches the site or the local
|
|
388
|
+
project files, and exits 3 (see [Exit codes](#exit-codes)).
|
|
389
|
+
|
|
390
|
+
`blocofy site migrate` is the one exception: it never resolves a target or a context at all (no
|
|
391
|
+
network, no identity call), so it leaves any `.blocofy/` binding in the directory completely
|
|
392
|
+
untouched.
|
|
393
|
+
|
|
394
|
+
## Exit codes
|
|
395
|
+
|
|
396
|
+
| Code | Meaning |
|
|
397
|
+
| --- | --- |
|
|
398
|
+
| 0 | Ok. |
|
|
399
|
+
| 1 | Usage / network / HTTP 5xx / a local check that refuses before any request (e.g. a `site migrate` conflict, a `pages migrate-layout` ambiguity). |
|
|
400
|
+
| 2 | The server refused the request (HTTP 4xx) — its `{error}` JSON is printed. |
|
|
401
|
+
| 3 | Target/binding refusal — nothing was read or written (see [Contexts and project binding](#contexts-and-project-binding)). |
|
|
402
|
+
| 4 | `site apply` only: not finished within its bounded pass count. Every step already applied is safe — re-run the same command to resume. |
|
|
403
|
+
|
|
404
|
+
`--json`: every failure prints `{"error":{"code","message","details"}}` as the **last** stderr
|
|
405
|
+
line; the target block (`{"target":…}`) and any warning lines are printed on stderr before it.
|
|
406
|
+
|
|
407
|
+
Retries: network errors and HTTP 429/502/503/504 are retried up to 3 times (`Retry-After`
|
|
408
|
+
honoured, max 30s per wait; else 0.3s/0.9s/2s), resending the identical request (`pages push`
|
|
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.
|
|
412
|
+
|
|
68
413
|
## Changelog
|
|
69
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.
|
|
471
|
+
- **0.10.0** — Named contexts + verified project binding, and a declarative whole-site state.
|
|
472
|
+
- `login` now saves a **named context** (`--context <name>`, default the site's slug) instead
|
|
473
|
+
of one global credentials pair; `blocofy contexts` / `use` / `logout` manage them, and
|
|
474
|
+
`blocofy link [dir] --context <name> [--adopt]` binds a project directory to one verified
|
|
475
|
+
site (`.blocofy/project.json`, committed; `.blocofy/local.json`, git-ignored).
|
|
476
|
+
`blocofy target [dir]` shows which site a command would hit without writing anything.
|
|
477
|
+
`login --keychain` keeps the secret in the macOS keychain instead of
|
|
478
|
+
`~/.blocofy/secrets.json`. A pre-0.10 `credentials.json` is migrated on first use (backup:
|
|
479
|
+
`~/.blocofy/credentials.v1.bak.json`).
|
|
480
|
+
- New `blocofy site export/validate/migrate/plan/apply/publish`: pull a whole site as one
|
|
481
|
+
declarative tree, check it fully offline, turn an older separate-pulls directory into that
|
|
482
|
+
tree layout (`site migrate`), ask what applying it would do, build it into a draft theme
|
|
483
|
+
version, and make it live. `site apply` is resumable — every step is idempotent and picks up
|
|
484
|
+
exactly what is left.
|
|
485
|
+
- `pages push` speaks page revision CAS: every pulled file carries `base_revision`, a push
|
|
486
|
+
previews its plan before writing, and a page changed on the site since the pull (or missing
|
|
487
|
+
`base_revision`) refuses with nothing changed instead of silently overwriting — recover with
|
|
488
|
+
`pages pull` + push again, or `--force --reason <text>`.
|
|
489
|
+
- `settings push` now **requires** `--instance <handle>` or `--live` — there is no longer an
|
|
490
|
+
implicit live write.
|
|
491
|
+
- Consistent exit codes across every command (see [Exit codes](#exit-codes)) and a `--json`
|
|
492
|
+
envelope (`{"error":{code,message,details}}`) as the last stderr line on any failure.
|
|
493
|
+
|
|
70
494
|
- **0.9.0** — Locale-aware page files for multilingual sites (needs a platform with page file protocol 2;
|
|
71
495
|
against an older server page commands stop with `PAGES_SERVER_UPGRADE_REQUIRED` and write nothing).
|
|
72
496
|
`pages pull` writes `pages/<locale>/index.json` and `pages/<locale>/routes/<path>/index.json`, so the same
|
|
@@ -148,11 +572,13 @@ editor — without affecting your published theme. Save a file and **every open
|
|
|
148
572
|
(the platform-rendered pages connect back to the local dev server's reload channel). Publish
|
|
149
573
|
the draft from the theme editor when you're ready.
|
|
150
574
|
|
|
151
|
-
Credentials
|
|
575
|
+
Credentials live in `~/.blocofy/credentials.json` (contexts, no secrets) + either
|
|
576
|
+
`~/.blocofy/secrets.json` (0600) or the macOS keychain — written by `blocofy login`, or the
|
|
152
577
|
`BLOCOFY_URL` + `BLOCOFY_TOKEN` environment variables (for CI/automation). The v1 API commands
|
|
153
|
-
(`pages media-uses`, `
|
|
154
|
-
(written by `blocofy login --api-key`) or `BLOCOFY_API_KEY`
|
|
155
|
-
together; with only one set the CLI fails instead of
|
|
578
|
+
(`pages media-uses`/`media-decide`, `site plan`/`export`/`publish`) use a separate
|
|
579
|
+
`blcf_live_…` key from the same file (written by `blocofy login --api-key`) or `BLOCOFY_API_KEY`
|
|
580
|
+
+ `BLOCOFY_API_URL` — both variables together; with only one set the CLI fails instead of
|
|
581
|
+
falling back to the file.
|
|
156
582
|
|
|
157
583
|
## Theme structure
|
|
158
584
|
|
|
@@ -165,12 +591,17 @@ A theme is a directory of files grouped by top-level folder:
|
|
|
165
591
|
| `block/` | Repeatable pieces used inside sections |
|
|
166
592
|
| `partial/` | Shared snippets (header/footer, …) |
|
|
167
593
|
| `asset/` | CSS and static files (`theme.css`) |
|
|
594
|
+
| `locales/` | Legacy Liquid translation files (`<tag>.json`, `<tag>.default.json`) |
|
|
168
595
|
| `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 |
|
|
169
|
-
| `config/settings.json` | Theme settings + color schemes |
|
|
170
|
-
| `config/settings_schema.json` | Theme settings panel schema (synced with `pull`/`push`) |
|
|
596
|
+
| `config/settings.json` | Theme settings + color schemes (`blocofy settings pull`; `settings push` needs `--instance <handle>` or `--live`) |
|
|
597
|
+
| `config/settings_schema.json` | Theme settings panel schema (synced with `theme pull`/`push`) |
|
|
171
598
|
|
|
172
599
|
Liquid templates use the `.liquid` extension; files under `asset/` are served as-is.
|
|
173
600
|
|
|
601
|
+
`blocofy site export` pulls the same content plus navigation, chrome and translations as one
|
|
602
|
+
declarative tree instead (see [Site (declarative state)](#site-declarative-state)); `blocofy
|
|
603
|
+
site migrate` turns a directory built from the commands above into that tree's layout.
|
|
604
|
+
|
|
174
605
|
## Development
|
|
175
606
|
|
|
176
607
|
Zero runtime dependencies (Node built-ins only). Run the tests with:
|