@blocofy/cli 0.14.0 → 0.16.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
@@ -5,9 +5,10 @@ instant preview, and publish. The CLI does **not** build assets — generate the
5
5
  own tools (npm/Vite/Tailwind); the platform serves plain Liquid + static assets.
6
6
 
7
7
  ```bash
8
- npx @blocofy/cli login # site URL + dev token (admin panel: Settings → Theme CLI tokens)
9
- cd path/to/theme
10
- npx @blocofy/cli theme dev # http://localhost:3030 — local theme + live data, livereload
8
+ npx @blocofy/cli login # browser login: pick your site, approve (draft-only)
9
+ npx @blocofy/cli init my-site # a theme work of its own + its files + a preview link; live site unchanged
10
+ cd my-site
11
+ npx @blocofy/cli theme push --draft --work <wk_…>
11
12
  ```
12
13
 
13
14
  `login` saves the site under a named **context** (default: the site's slug) in
@@ -20,18 +21,44 @@ targets the right one — see [Contexts and project binding](#contexts-and-proje
20
21
  ### Login & contexts
21
22
 
22
23
  ```
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.
24
+ blocofy login [--context <name>] [--site <handle|slug>] [--api-url <url>] [--no-browser] [--insecure-storage]
25
+ ```
26
+ **Browser login (default, 0.16.0).** The CLI opens your system browser on the platform
27
+ (Authorization Code + PKCE S256), you pick the site and approve; the browser comes back to a
28
+ one-shot listener on `http://127.0.0.1:<port>/callback` (or `[::1]`; `localhost` is never used).
29
+ The CLI checks `state` and the RFC 9207 `iss` against the platform's discovery metadata before
30
+ it exchanges the code, and says "connected" only after `GET /api/v1/ping` confirms the site, the
31
+ profile and the CLI audience. The login is **draft-only** ("Tema geliştirme", `theme-dev@1`): it
32
+ can start and fill theme works, push to its own work's draft and create preview links, but it can
33
+ never change the live site or publish — publishing is the panel's (or a human approval's) job.
34
+ - Storage: the OS secure store — macOS Keychain, Windows DPAPI-protected file
35
+ (`%APPDATA%\blocofy\secrets.dpapi`), Linux Secret Service (`secret-tool`). With no OS store the
36
+ login is **refused**, never silently written to a plain file; `--insecure-storage` (or
37
+ `BLOCOFY_SECRET_STORE=file`) chooses `~/.blocofy/secrets.json` (0600) explicitly and is announced.
38
+ - The access token lives 10 minutes and renews itself with a single-use refresh token, one
39
+ renewal at a time across every running `blocofy` (lock `~/.blocofy/.refresh-<context>.lock`,
40
+ `/token` timeout 10 s). A refused renewal (revoked, expired, reused) marks the context
41
+ `reauth_required`: run `blocofy login --context <name>` again.
42
+ - `--site` refuses (and revokes) a login for any other site; `--no-browser` prints the URL only;
43
+ `--api-url` names the platform (default `https://app.blocofy.com`).
44
+ - Needs a terminal. CI (`CI` set, no TTY) and SSH sessions never open a browser: use env
45
+ credentials or the advanced options below.
46
+
47
+ ```
48
+ blocofy login --url <url> --token [--context <name>] [--keychain] # advanced: pasted dev token
49
+ ```
50
+ Verify a dev token (`bcf_…`, or a profiled `bcf2_…` — needs CLI 0.16.0+) against its site
51
+ (`GET /api/dev/whoami`) and save it as a named context (default name: the site's slug). Nothing
52
+ is saved if verification fails. `--token` without a value reads it from a hidden prompt;
53
+ `--token <value>` still works but warns (`TOKEN_IN_ARGV`: argv is visible in `ps` and shell
54
+ history). Get a token from the admin panel → Settings → Theme CLI tokens.
28
55
  - `--keychain` — keep the secret in the macOS keychain (or `BLOCOFY_SECRET_STORE=keychain`);
29
- default is `~/.blocofy/secrets.json` (0600).
56
+ default is `~/.blocofy/secrets.json` (0600). The output names the store used.
30
57
 
31
58
  ```
32
59
  blocofy login --api-key [--context <name>] [--api-url <url>]
33
60
  ```
34
- Verify a v1 API key (`blcf_live_…`, `GET /api/v1/ping`) and add it to a context. The key is
61
+ Verify a v1 API key (`blcf_live_…` or a profiled `blcf_k2_…`, `GET /api/v1/ping`) and add it to a context. The key is
35
62
  read from a **hidden prompt** — the flag takes no value, so it never lands in argv or shell
36
63
  history. If the context already has a dev token for another site, nothing is saved
37
64
  (`TARGET_CREDENTIAL_MISMATCH`).
@@ -41,8 +68,43 @@ history. If the context already has a dev token for another site, nothing is sav
41
68
  ```
42
69
  blocofy contexts [--json] # list saved contexts (never prints secrets)
43
70
  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
- ```
71
+ blocofy logout [--context <name>] # remove a context and its secrets (default: the `use` context)
72
+ ```
73
+ `logout` reports two separate results. **Server** (browser logins only): `revoked` (the platform's
74
+ RFC 7009 `/revoke` answered 200), `unreachable` / `refused` / `unavailable` (NOT revoked — the
75
+ output names the panel page where you cut it: Settings → Connections and keys), or
76
+ `not_applicable` (a pasted token/key: revoke it in the panel). **Local**: `cleared` | `failed`,
77
+ done whatever the server said. Exit 0 only when both are done; otherwise exit 4.
78
+
79
+ ### New project: `blocofy init`
80
+
81
+ ```
82
+ blocofy init [dir] [--site <handle|slug>] [--context <name>] [--api-url <url>] [--no-browser] [--insecure-storage]
83
+ ```
84
+ Sets up a new project directory end to end, without touching the live site:
85
+ 1. Pre-checks, before any request: a missing or empty directory starts; a directory this CLI
86
+ initialised resumes; a non-empty directory that is not a project is refused
87
+ (`INIT_DIR_NOT_EMPTY`, exit 3, nothing written); a directory bound to another site is refused
88
+ (`INIT_SITE_MISMATCH`, exit 3); a project bound by `link`/`pull` is not taken over
89
+ (`INIT_ALREADY_BOUND`).
90
+ 2. The credential: `--context`, env credentials, the directory's saved context, or (on a terminal)
91
+ a browser login. Non-interactive runs never open a browser and need `--site`.
92
+ 3. Reads the live theme, starts a theme work (`POST /api/v1/theme-work`, idempotency key
93
+ `init:<key>` from `.blocofy/init.json` — written before the first request), downloads the
94
+ work's theme files, writes `.blocofy/project.json` (+ the additive `profile`) and
95
+ `.blocofy/local.json` (context + the work), creates a preview link for the work's home page,
96
+ and reads the live theme again.
97
+ 4. Success = the preview link exists **and** the live theme read back is the one read before:
98
+ "Taslak hazır. Canlı siten değişmedi." If the live theme changed meanwhile (not by init),
99
+ nothing is called a success: exit 5.
100
+
101
+ `.blocofy/init.json` (git-ignored, no secret, no link) records the step reached
102
+ (`started → work_created → files_pulled → pinned → previewed`). Run the same command again after a
103
+ timeout, a crash or Ctrl-C: it reads the work back from the platform and continues — the same key
104
+ always returns the same work, never a second one. Two `init`s in one directory at once: one runs,
105
+ the other is refused (`INIT_IN_PROGRESS`). Do not delete `init.json` to "start over": use a new,
106
+ empty directory.
107
+
46
108
 
47
109
  ### Project binding
48
110
 
@@ -80,8 +142,10 @@ changing credentials, context or the project binding).
80
142
  blocofy theme pull [dir] [--draft] [--instance <handle>]
81
143
  ```
82
144
  Download the live theme to disk. `dir` defaults to cwd.
83
- - `--draft` — pull the draft theme (what `theme dev` syncs into) instead of live; creates the
84
- draft if missing, so it needs a bound project.
145
+ - `--draft` — pull the CLI draft (what `theme dev` and `theme push --draft` write into) instead of
146
+ live. Read-only: it never creates the draft. With no CLI draft yet the platform answers
147
+ `target_missing` and the CLI stops (exit 2, nothing written) and tells you to run
148
+ `blocofy theme push --draft` first. It still needs a bound project.
85
149
  - `--instance <handle>` — pull a specific theme by its handle (admin panel theme card, or
86
150
  `blocofy status`).
87
151
 
@@ -130,6 +194,20 @@ create a new draft theme in the admin panel and pass its handle with `--instance
130
194
  delete the restore draft first. `theme publish` without `--instance` uses the same rule and never
131
195
  publishes a guessed draft. With no draft at all, the first push creates one as before.
132
196
 
197
+ **When the platform cannot create the new draft.** A draft push (or the `theme dev` sync) that needs
198
+ a new CLI draft can be refused by the platform's capacity check. Nothing is written in any of these
199
+ cases; the CLI explains it in plain words first, then prints the `error [code]` line (the `--json`
200
+ envelope keeps the code and details):
201
+ - `quota_exceeded` (exit 2) — worded by `details.resource`, with the usage the platform reports: the
202
+ site's theme space is full (`theme_bytes`) or a site not on theme space reached its old draft count
203
+ (`theme_drafts`) — remove a draft you no longer need (neither is a plan limit); the plan's storage
204
+ is full (`storage_mb`) — the plan message.
205
+ - `capacity_unavailable` / `resource_busy` (exit 1) — temporary. The CLI has already retried within
206
+ its normal retry policy (waiting as long as the server's `Retry-After` asks); try again later.
207
+ - `source_stale` (exit 2) — the live theme the new draft copies changed meanwhile; run the command
208
+ again.
209
+ `theme dev` reports each of them in one line and tries again on the next save.
210
+
133
211
  After a draft push the CLI prints the draft it wrote to (`Draft: <handle> "<name>" (new)` or
134
212
  `(existing CLI draft, updated)`) and the `blocofy theme publish --instance <handle>` command.
135
213
  - `--idempotency-key <k>` — attach a key so a retried push is not double-applied.
@@ -184,6 +262,65 @@ theme publish ./shop` always publishes `./shop`'s site, whatever directory you r
184
262
  site's CLI draft; any other theme (a site-state restore draft, a renamed or panel-copied theme) is
185
263
  refused with `publish_target_unconfirmed` (exit 2, nothing published) — name it with `--instance`.
186
264
 
265
+ ### Theme work (new)
266
+
267
+ A **work** is a private, safe copy of the site's current live theme: you change it, visitors never
268
+ see it, and a person approves its publication in the admin panel (the CLI never publishes a work).
269
+ Work commands use the **v1 API key** (`blocofy login --api-key`, or `BLOCOFY_API_KEY` +
270
+ `BLOCOFY_API_URL`): the work belongs to the key that started it. The dev token alone is refused
271
+ (`LOGIN_REQUIRED`) before any request.
272
+
273
+ ```
274
+ blocofy theme work start [dir] [--intent "<text>"] [--idempotency-key <k>]
275
+ blocofy theme work status <wk_…> [--dir <dir>]
276
+ blocofy theme work resume <wk_…> [--dir <dir>] [--require-fresh]
277
+ blocofy theme work cancel <wk_…> [--dir <dir>]
278
+ blocofy theme work seal <wk_…> [--dir <dir>]
279
+ blocofy theme work request-approval <wk_…> [--dir <dir>] [--open] [--wait [--interval <s>]]
280
+ blocofy theme push [dir] --draft --work <wk_…>
281
+ ```
282
+ - `seal` prepares the work for review: its content is frozen and can no longer change. It asks for
283
+ no approval and publishes nothing. On a platform without this step it says so (`seal_unsupported`,
284
+ exit 2); `request-approval` prepares the work itself.
285
+ - `request-approval` asks a person to publish the work (an open work is prepared for review first)
286
+ and prints the **approval URL**. A signed-in site owner, or a team member with the theme permission,
287
+ approves on that Blocofy page. The URL carries no token: having it is not a permission to publish.
288
+ `--open` also opens it in your browser (only when it is this platform's approval page). `--wait`
289
+ polls the read-only publish status until the work is published or the request ends; the CLI never
290
+ publishes a work. How `--wait` ends:
291
+
292
+ | Exit | Code (`--json` envelope) | Meaning |
293
+ | --- | --- | --- |
294
+ | 0 | — | Approved and published; the work is live. |
295
+ | 2 | `approval_stale` | The live site changed after the request; nothing published. Start a new work from the current live site and ask again. |
296
+ | 2 | `approval_expired` | Nobody decided within 15 minutes; nothing published. Ask again. |
297
+ | 2 | `approval_declined` | The request was declined; nothing published. |
298
+ | 2 | `approval_superseded` | The work was reopened for changes; this request is no longer valid. Ask again. |
299
+ | 2 | `work_cancelled` | The work was cancelled; nothing published. |
300
+ | 1 | `publish_failed` | The work could not be completed; nothing published. Check `theme work status`. |
301
+ | 1 | `wait_timeout` | No decision yet; the CLI stopped waiting. The request stays valid until it expires. |
302
+
303
+ A refusal before the wait starts (for example `work_state_conflict`) exits as usual (2 for a 4xx,
304
+ 1 for a 5xx); see [Exit codes](#exit-codes).
305
+ - `status` also shows where the publication stands (preparing, ready for review, waiting for
306
+ approval with its URL, published, needs update).
307
+ - `start` prints the work's handle (`wk_…`) and saves it in `.blocofy/local.json` (git-ignored; the
308
+ handle alone grants nothing). It sends an `Idempotency-Key` (yours, or a printed `cli-work-<uuid>`):
309
+ the same key returns the same work, never a second one. When the answer is lost (network, 5xx),
310
+ the CLI prints the exact command to run again with the same key.
311
+ - `status`, `resume` and `cancel` always take the handle; with none they suggest the project's saved
312
+ work and stop. `status` and `resume` say whether the site changed since the work started; a changed
313
+ site is never overwritten. `resume --require-fresh` refuses (`work_stale`) instead.
314
+ - `cancel` reads the work's `state_version` and sends it; the live site is untouched and the work's
315
+ theme stays in the theme library.
316
+ - `theme push --work <wk_…>` writes into that work's own draft theme (`--work` implies `--draft`; it
317
+ cannot be combined with `--live` or `--instance`). It needs **both** the dev token and the API key of
318
+ the same site, and refuses unless the work is open (`work_state_conflict`, nothing written).
319
+ - Refusals print a plain Turkish explanation first, then the `error [code]` line (`--json`: the
320
+ envelope only): `not_found`, `work_forbidden`, `work_state_conflict`, `work_stale`, `work_sealed`,
321
+ `work_base_unavailable`, `quota_exceeded`, `capacity_unavailable`, `resource_busy`,
322
+ `idempotency_key_reuse` (exit 2 for a 4xx, 1 for a 5xx).
323
+
187
324
  ### Status
188
325
 
189
326
  ```
@@ -469,7 +606,8 @@ untouched.
469
606
  | 1 | Usage / network / HTTP 5xx / a local check that refuses before any request (e.g. a `site migrate` conflict, a `pages migrate-layout` ambiguity). |
470
607
  | 2 | The server refused the request (HTTP 4xx) — its `{error}` JSON is printed. |
471
608
  | 3 | Target/binding refusal — nothing was read or written (see [Contexts and project binding](#contexts-and-project-binding)). |
472
- | 4 | `site apply` only: not finished within its bounded pass count. Every step already applied is safe — re-run the same command to resume. |
609
+ | 4 | `site apply`: not finished within its bounded pass count. Every step already applied is safe — re-run the same command to resume. `logout`: the local login could not be removed, or the platform login was NOT revoked (cut it in the panel). |
610
+ | 5 | `init`: the live theme changed while init ran (init never writes it) — not reported as a success. |
473
611
 
474
612
  `--json`: every failure prints `{"error":{"code","message","details"}}` as the **last** stderr
475
613
  line; the target block (`{"target":…}`) and any warning lines are printed on stderr before it.
@@ -485,6 +623,82 @@ Each retry prints a notice on stderr.
485
623
 
486
624
  ## Changelog
487
625
 
626
+ - **0.16.0** (unreleased) — Browser login, `blocofy init`, honest `logout` (ADR-0014 wave P4).
627
+ Pairs with the platform release of the credential profiles (draft-only "Tema geliştirme" by
628
+ default). **Minimum platform:** the browser login needs a platform that publishes the CLI client
629
+ (`blocofy-cli`), RFC 9207 `iss`, `/revoke` and the `profile` / `audience` / `dev_endpoint` fields of
630
+ `GET /api/v1/ping`; an older platform is told apart and refused with "use `login --token`".
631
+ - **`blocofy login` opens the browser** (PKCE S256, loopback `127.0.0.1`/`[::1]` only, `state`
632
+ and `iss` checked, `resource=<origin>/api/v1`). Connected = the identity probe, not a config
633
+ write. Unattended (CI / no TTY) and SSH runs never open a browser. The pasted dev token stays as
634
+ the advanced path (`login --url <site> --token`, hidden prompt; a token in argv warns).
635
+ - **Secure storage**: OS store by default (Keychain / DPAPI / Secret Service); no store → refused;
636
+ the 0600 file only with `--insecure-storage` / `BLOCOFY_SECRET_STORE=file`. **Unverified on real
637
+ machines:** the Windows (DPAPI via PowerShell) and Linux (Secret Service via `secret-tool`) stores
638
+ are covered only by tests with a stand-in for those programs; macOS Keychain uses the existing,
639
+ field-tested adapter.
640
+ - **Draft-only logins default to their saved work**: for a browser login (or any draft-only profile)
641
+ `theme push` and `theme dev` write into the theme work saved by `init` / `theme work start`
642
+ (`.blocofy/local.json`, else `.blocofy/init.json`); `--work` / `--instance` still win. Without a
643
+ saved work they stop with a hint (`THEME_WORK_REQUIRED`, exit 3) before any request.
644
+ - **Refresh single-flight** across processes (lock with pid/hostname/acquired_at, ESRCH or 30 s
645
+ staleness, 10 s `/token` timeout, never retried); reuse/revocation → `reauth_required`. The
646
+ platform's explicit `error_description: "refresh_token_reused"` is honoured first; a 503 without
647
+ `Retry-After` is treated as spent only as a fallback for older platforms.
648
+ - **`init` preview**: uses the work-level `POST /api/v1/theme-work/{handle}/preview-links` when the
649
+ platform has it (an older platform's 404/405 falls back to finding the work theme's home page).
650
+ - **`blocofy logout`** reports the platform revocation and the local removal separately (exit 4
651
+ when either is not done); `--context` defaults to the `use` context.
652
+ - **`blocofy init [dir]`** (see above): idempotent theme-work start, secret-free
653
+ `.blocofy/init.json`, refuses dirty / other-site directories, resumes from every step, success
654
+ only with a preview and the live theme read back unchanged (exit 5 otherwise).
655
+ - **New platform refusals in plain Turkish**, printed before the usual `error [code]` line and
656
+ never retried: `live_effect_not_permitted`, `credential_reapproval_required` (due date +
657
+ re-approval link), `profile_unsupported`, `audience_mismatch`, `ai_draft`,
658
+ `work_copy_not_writable`.
659
+ - **Profiled credentials**: `bcf2_` dev tokens and `blcf_k2_` API keys are accepted (0.15 and
660
+ older refuse `bcf2_` tokens: upgrade the CLI before using one).
661
+ - **0.15.0** (2026-10-05) — Theme space, theme work and human-approved
662
+ publishing. Pairs with the platform release that replaces the 5-draft limit with a per-site theme
663
+ space.
664
+ - **Capacity refusals in plain words.** A theme write refused by the platform explains why in
665
+ plain Turkish first, then prints the usual `error [code]` line (`--json`: the envelope only):
666
+ - `quota_exceeded` (exit 2) is worded by `details.resource`. `theme_bytes`: the site's theme space
667
+ is full — on any push, a push to an existing draft with `--instance` included. It shows used /
668
+ allowance from `details`, says to free space by deleting unused draft themes from the theme
669
+ library (cancelled works' themes included; cancelling an open work also releases its reserved
670
+ publish space; an archived work's theme still counts), or to contact support, and never suggests a plan upgrade or says "new draft" (theme space is not
671
+ a plan quota). `theme_drafts` (the old draft count, on sites that have not moved to theme space)
672
+ is not a plan limit either: it says to remove a draft you no longer need, never to upgrade.
673
+ Only `storage_mb` keeps the plan message. The envelope carries the server's `details`.
674
+ - `work_revision_limit` (exit 2; `theme work seal`, or `request-approval` when it prepares the
675
+ work): a work can be sent for review at most 20 times (`details.limit`); start a new work.
676
+ - `capacity_unavailable` / `resource_busy` (exit 1, after the usual retries that honour
677
+ `Retry-After`; the envelope carries `retryAfterSeconds`): a temporary refusal, try again later.
678
+ A 503 on the write is no longer reported as "outcome unknown" (the platform wrote nothing),
679
+ unless an earlier attempt of the same push got no answer.
680
+ - `source_stale` (exit 2): the live theme changed while a new draft was copied; run it again.
681
+ - `theme dev` reports the same refusals in one line per save.
682
+ - **`theme pull --draft` is read-only.** Help and README no longer claim it creates the draft. With
683
+ no CLI draft the platform answers 404 `target_missing`; the CLI tells you to run
684
+ `blocofy theme push --draft` first (exit 2, nothing written). The command still needs a bound
685
+ project (kept for platforms from before this change).
686
+ - **New: theme work.** `theme work start|status|resume|cancel` and `theme push --work <wk_…>`
687
+ (v1 API key; see "Theme work"). A work is a private copy of the current live theme, named only by
688
+ its handle; the handle is saved in `.blocofy/local.json`. A plain `theme push` is unchanged and
689
+ never calls the work API.
690
+ - **New: human-approved publishing.** `theme work seal` (prepare for review, asks for no approval)
691
+ and `theme work request-approval [--open] [--wait [--interval <s>]]`: prints the approval URL (no
692
+ token in it), optionally opens it, and `--wait` polls the status read-only — exit 0 when
693
+ published, 2 when the request ends without a publication (`approval_stale`, `approval_expired`,
694
+ `approval_declined`, `approval_superseded`, `work_cancelled`), 1 on `wait_timeout` or
695
+ `publish_failed` (table under "Theme work"). `theme work status` shows the publish status. The CLI
696
+ never publishes a work.
697
+ - **`theme publish` names a work copy's refusal.** Publishing a theme work's copy directly is refused
698
+ by the platform (`work_not_publishable`, exit 2, nothing published); the CLI now keeps that code
699
+ (it printed `HTTP_409` before) and says to send the work for review and ask for approval.
700
+ - Unchanged: retry classes, `draft_target_ambiguous` handling, idempotency keys, target guards and
701
+ protocol headers.
488
702
  - **0.14.0** — Draft commands no longer guess their draft (platform #989).
489
703
  - **Behaviour change:** `theme push` (draft), `theme push --diff`, `theme pull --draft` and the
490
704
  `theme dev` sync refuse, writing nothing, when the platform answers `draft_target_ambiguous` (or the