@blocofy/cli 0.12.0 → 0.13.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
@@ -63,13 +63,15 @@ Show which site a command in `[dir]` would hit (verified), without writing anyth
63
63
  ```
64
64
  blocofy theme dev [dir] [--port <n>] [--no-sync] [--name <name>]
65
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.
66
+ Start a local dev server that renders your local theme files with the site's live content.
67
+ Press `l` to open it, `q` to quit. Edit a file and save → the view reloads. Saves sync to a
68
+ **draft** theme only (never the live site). `dir` defaults to cwd. The platform's remote preview
69
+ and editor views for this command are retired; to share a draft page, create a preview link for
70
+ it (v1 API `POST /themes/{id}/preview-links` or MCP `create_preview_link`; one page per link).
69
71
  The target site is verified once at start; a long session keeps that target (restart it after
70
72
  changing credentials, context or the project binding).
71
73
  - `--port <n>` — local port (default 3030).
72
- - `--no-sync` — local preview only (skip draft sync + remote views).
74
+ - `--no-sync` — local preview only (skip draft sync).
73
75
  - `--name <name>` — name the draft when it is first created (ignored if it already exists).
74
76
 
75
77
  ```
@@ -94,13 +96,20 @@ the admin panel, never touching the live site. Publish it with `blocofy theme pu
94
96
  - `--yes` — confirm a `--live` push without prompting (CI/agents).
95
97
  - `--instance <handle>` — push to a specific theme by its handle (safe targeted write — no
96
98
  live-confirmation prompt).
97
- - `--name <name>` — name the new draft (draft mode only; ignored on `--live`/`--instance`).
99
+ - `--name <name>` — name the new draft (draft mode only; ignored on `--live`/`--instance`). A draft
100
+ push reuses the existing CLI draft whatever its name; then the push says `--name` was not applied
101
+ and prints the `blocofy theme rename` command for it.
98
102
  - `--dry-run` / `--validate` — check the push on the server without writing; the two flags are
99
103
  aliases. The dry run covers exactly the files the push would send (remote-only files it keeps
100
104
  included; with `--prune`, without the ones it would remove). A current platform plans the whole
101
105
  deploy and prints the target, its pointer version and what happens to each file
102
106
  (`+` created, `~` updated, `-` removed; unchanged files are counted).
103
- - `--diff` — show what a push would change vs the target (read-only), then stop.
107
+ - `--diff` — show what a push would change vs the target (read-only), then stop. A draft push
108
+ compares with the CLI draft it writes to (named by handle); with no CLI draft yet, with the live
109
+ theme (the push would create the draft). `--live` compares with live, `--instance` with that theme.
110
+
111
+ After a draft push the CLI prints the draft it wrote to (`Draft: <handle> "<name>" (new)` or
112
+ `(existing CLI draft, updated)`) and the `blocofy theme publish --instance <handle>` command.
104
113
  - `--idempotency-key <k>` — attach a key so a retried push is not double-applied.
105
114
  - `--prune` — also remove target files that no longer exist locally (`locales/` included);
106
115
  lists them first, and on the live theme asks to confirm (non-interactive shells add `--yes`).
@@ -452,6 +461,17 @@ Each retry prints a notice on stderr.
452
461
 
453
462
  ## Changelog
454
463
 
464
+ - **0.13.0** — Draft push names its target (customer items PS-22/PS-23/PS-26).
465
+ - `theme push` (draft) prints the draft it wrote to — handle, name, new or existing — and
466
+ `blocofy theme publish --instance <handle>`. When it updated the existing CLI draft, it says
467
+ `--name` was not applied and prints the rename command.
468
+ - **Behaviour change:** `theme push --diff` without `--live`/`--instance` now compares with the CLI
469
+ draft the push writes to (found read-only via the site status, never provisioned), not with the
470
+ live theme. With no CLI draft yet it says so and compares with live. Release it after the
471
+ platform's PS-22 fix (uncached theme reads): before that fix a draft read could return its
472
+ pre-push files for up to five minutes.
473
+ - `theme dev` help no longer lists the retired live-domain and editor views.
474
+ - `TARGET_BINDING_REQUIRED` also names `blocofy link <dir>` for env credentials.
455
475
  - **0.12.0** — Translation packages (#925).
456
476
  - `blocofy translations export --locale <tag> --out <file> [--format json|xliff] [--only …]`
457
477
  writes every text of one language that needs translating to one JSON or XLIFF 1.2 file. It
@@ -619,11 +639,9 @@ files and sends them to the platform's dev-render endpoint (`/api/dev/render`).
619
639
  renders them with the site's **live data** and returns HTML — so the CLI ships no rendering
620
640
  engine and you see exactly the production output.
621
641
 
622
- It also continuously syncs your local files to a **draft theme** so you can view the same work
623
- three ways — the local preview, a shareable live-domain preview link, and the admin theme
624
- editor — without affecting your published theme. Save a file and **every open view reloads**
625
- (the platform-rendered pages connect back to the local dev server's reload channel). Publish
626
- the draft from the theme editor when you're ready.
642
+ It also continuously syncs your local files to a **draft theme**, so the same work can be opened
643
+ in the admin theme editor without affecting your published theme. Save a file and the local view
644
+ reloads. Publish the draft from the theme editor, or with `blocofy theme publish`, when you're ready.
627
645
 
628
646
  Credentials live in `~/.blocofy/credentials.json` (contexts, no secrets) + either
629
647
  `~/.blocofy/secrets.json` (0600) or the macOS keychain — written by `blocofy login`, or the
package/bin/blocofy.mjs CHANGED
@@ -71,7 +71,7 @@ import { MANIFEST_PATH, buildManifest, validateSiteStateTree, verifyManifest } f
71
71
  import { SiteStateFsError, hashBuffer, readSiteStateTree, stagedWriteTree } from "../lib/site-state-fs.mjs";
72
72
  import { migrateSiteState } from "../lib/site-migrate.mjs";
73
73
  import { applySiteState, downloadAssetBytes, fetchSiteStateExport, planSiteState, publishSiteState, uploadMediaAsset } from "../lib/site-state-client.mjs";
74
- import { diffTheme, fetchCanonicalSupport, fetchDevSession, fetchSiteStatus, publishInstance, pullTheme, pushTheme, renameInstance } from "../lib/theme-sync.mjs";
74
+ import { diffTheme, fetchCanonicalSupport, fetchDevSession, fetchSiteStatus, findCliDraft, publishInstance, pullTheme, pushTheme, renameInstance } from "../lib/theme-sync.mjs";
75
75
  import { isAffirmative, livePushDecision, resolvePushMode } from "../lib/confirm.mjs";
76
76
  import { hyperlink, openUrl } from "../lib/term.mjs";
77
77
  import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
@@ -178,14 +178,15 @@ Usage
178
178
  Show which site a command in [dir] would hit (verified), without writing anything.
179
179
 
180
180
  blocofy theme dev [dir] [--port <n>] [--no-sync] [--name <name>]
181
- Start a dev server and print 3 auto-reloading views — Local, live-domain
182
- Preview, and the theme Editor. Press l / p / e to open each, q to quit.
183
- Edit a file and save → every open view reloads. Saves sync to a DRAFT theme
184
- only (never the live site). (dir defaults to cwd)
181
+ Start a local dev server that renders your local theme files with the site's
182
+ live content. Press l to open it, q to quit. Edit a file and save → the view
183
+ reloads. Saves sync to a DRAFT theme only (never the live site). (dir defaults to cwd)
184
+ The platform's remote preview and editor views for this command are retired; to
185
+ share a draft page, create a preview link for it (v1 API / MCP create_preview_link).
185
186
  The target site is verified ONCE at start; a long session keeps that target (restart
186
187
  it after changing credentials, context or the project binding).
187
188
  --port <n> local port (default 3030)
188
- --no-sync local preview only (skip draft sync + remote views)
189
+ --no-sync local preview only (skip draft sync)
189
190
  --name <name> name the draft when it is first created (ignored if it already exists)
190
191
 
191
192
  blocofy theme pull [dir] [--draft] [--instance <handle>]
@@ -952,7 +953,7 @@ async function themePush(rest) {
952
953
  // CF-T2: `--diff` and `--dry-run` are reads; every other push is a remote mutation (binding required). The
953
954
  // target is verified (whoami, both pairs, binding) before the first theme request — no best-effort swallow.
954
955
  const readOnly = Boolean(flags.diff) || dryRun;
955
- const opMode = flags.diff ? `read · diff vs ${instanceFlag ? `instance ${instanceFlag}` : "live"}` : dryRun ? `read · dry run (${mode === "instance" ? `instance ${instance}` : mode})` : mode === "instance" ? `instance ${instance}` : mode;
956
+ const opMode = flags.diff ? `read · diff vs ${instanceFlag ? `instance ${instanceFlag}` : mode === "draft" ? "CLI draft" : "live"}` : dryRun ? `read · dry run (${mode === "instance" ? `instance ${instance}` : mode})` : mode === "instance" ? `instance ${instance}` : mode;
956
957
  const target = await prepareTarget({ command: "theme push", commandClass: readOnly ? "read" : "remote-mutation", dir, flags, needs: "dev", mode: `${opMode}${flags.prune && !readOnly ? " · prune" : ""}` });
957
958
  const creds = target.dev;
958
959
  // 0.5.0: her push'a otomatik idempotency key — kanonik dal (protokol + key) ancak böyle seçilir; key
@@ -963,12 +964,18 @@ async function themePush(rest) {
963
964
  // ad-alanlar, önek EKLENMEZ.
964
965
  const idempotencyKey = typeof flags["idempotency-key"] === "string" ? flags["idempotency-key"] : `cli-${randomUUID()}`;
965
966
 
966
- // `--diff`: read-only preview vs the LIVE theme (or --instance). No write; the draft target is not
967
- // diffable (a draft GET would PROVISION the draft server-side — a read-only command must not mutate).
967
+ // `--diff`: read-only preview vs what the push would write to. PS-26: a draft push writes to the CLI draft, so
968
+ // that draft is found via `/api/dev/site` and diffed by handle — never via `?draft=1`, which PROVISIONS a draft
969
+ // server-side (a read-only command must not mutate). With no CLI draft yet the push would create one: vs live.
968
970
  if (flags.diff) {
969
971
  const diffTarget = ` of ${siteLabel(target.identity.site)}`;
970
- const d = await diffTheme({ dir, url: creds.url, token: creds.token, instance: instanceFlag, onRetry });
971
- console.log(instanceFlag ? `Diff vs theme ${instanceFlag}${diffTarget}:` : `Diff vs the LIVE theme${diffTarget} (push default writes to a DRAFT):`);
972
+ const cliDraft = mode === "draft" ? findCliDraft(await fetchSiteStatus({ url: creds.url, token: creds.token, onRetry })) : null;
973
+ const diffInstance = instanceFlag ?? cliDraft?.id ?? null;
974
+ const d = await diffTheme({ dir, url: creds.url, token: creds.token, instance: diffInstance, onRetry });
975
+ if (instanceFlag) console.log(`Diff vs theme ${instanceFlag}${diffTarget}:`);
976
+ else if (cliDraft) console.log(`Diff vs draft ${cliDraft.id}${cliDraft.name ? ` "${cliDraft.name}"` : ""}${diffTarget} (the CLI draft this push writes to):`);
977
+ else if (mode === "draft") console.log(`No CLI draft yet — this push would create one. Diff vs the LIVE theme${diffTarget}:`);
978
+ else console.log(`Diff vs the LIVE theme${diffTarget}:`);
972
979
  const total = d.added.length + d.changed.length + d.removed.length;
973
980
  if (total === 0) {
974
981
  console.log("No differences — local theme matches the target.");
@@ -1140,8 +1147,15 @@ async function themePush(rest) {
1140
1147
  }
1141
1148
  if (result.convergence === "converged") console.log(" (already applied by an earlier push with the same idempotency key)");
1142
1149
  if (mode === "draft") {
1150
+ const h = typeof result.targetInstance === "string" ? result.targetInstance : null;
1151
+ const label = result.targetName ? ` "${result.targetName}"` : "";
1152
+ if (h) console.log(result.newDraft ? `Draft: ${h}${label} (new)` : `Draft: ${h}${label} (existing CLI draft, updated)`);
1153
+ // PS-22: the server reuses the CLI draft whatever its name; --name only names a draft this push creates.
1154
+ if (name && result.newDraft !== true) {
1155
+ console.log(`Note: --name was not applied — this push updated the existing CLI draft${h ? ` ${h}` : ""}. Rename it with: blocofy theme rename ${h ?? "<handle>"} "${name}"`);
1156
+ }
1143
1157
  console.log("Preview & publish it in the admin panel: Theme -> Theme library -> \"Open in editor\".");
1144
- console.log("Publish it live with: blocofy theme publish");
1158
+ console.log(`Publish it live with: blocofy theme publish${h ? ` --instance ${h}` : ""}`);
1145
1159
  }
1146
1160
  return;
1147
1161
  }
package/lib/target.mjs CHANGED
@@ -217,13 +217,16 @@ function isEmptyOrMissing(dir) {
217
217
  * Command-class policy (before any credential or network use). Returns `{ newBinding }`: true when a local-write
218
218
  * into a missing/empty unbound dir will record provenance after it succeeds.
219
219
  */
220
+ /** PS-22 note: with env credentials there is no context to name — `link` binds to the site those credentials reach. */
221
+ const ENV_LINK_HINT = (dir) => `; with BLOCOFY_URL + BLOCOFY_TOKEN set: blocofy link ${dir}`;
222
+
220
223
  export function enforceBindingPolicy({ commandClass, binding, dir, command }) {
221
224
  if (!COMMAND_CLASSES.has(commandClass)) throw new Error(`internal: unknown command class ${commandClass}`);
222
225
  if (binding) return { newBinding: false };
223
226
  if (commandClass === "remote-mutation") {
224
227
  throw new TargetError(
225
228
  "TARGET_BINDING_REQUIRED",
226
- `\`${command}\` changes a site, but ${dir} is not bound to one. Nothing was sent. Bind it first: blocofy link ${dir} --context <name>`,
229
+ `\`${command}\` changes a site, but ${dir} is not bound to one. Nothing was sent. Bind it first: blocofy link ${dir} --context <name>${ENV_LINK_HINT(dir)}`,
227
230
  { dir },
228
231
  );
229
232
  }
@@ -231,7 +234,7 @@ export function enforceBindingPolicy({ commandClass, binding, dir, command }) {
231
234
  if (isEmptyOrMissing(dir)) return { newBinding: true };
232
235
  throw new TargetError(
233
236
  "TARGET_BINDING_REQUIRED",
234
- `${dir} is not empty and not bound to a site. Nothing was written. Bind it first (blocofy link ${dir} --context <name>) or pull into an empty directory.`,
237
+ `${dir} is not empty and not bound to a site. Nothing was written. Bind it first (blocofy link ${dir} --context <name>${ENV_LINK_HINT(dir)}) or pull into an empty directory.`,
235
238
  { dir },
236
239
  );
237
240
  }
@@ -322,9 +322,10 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
322
322
  const remoteOnlyKept = [];
323
323
  const remoteOnlyRemoved = [];
324
324
  let probeInstance = instance;
325
+ let cliDraft = null;
325
326
  if (!instance && draft) {
326
- const status = await fetchSiteStatus({ url, token, onRetry });
327
- probeInstance = (status?.drafts ?? []).find((d) => d.source === "import")?.id ?? null;
327
+ cliDraft = findCliDraft(await fetchSiteStatus({ url, token, onRetry }));
328
+ probeInstance = cliDraft?.id ?? null;
328
329
  }
329
330
  let remote = {};
330
331
  if (probeInstance || !draft) {
@@ -391,7 +392,22 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
391
392
  throw applyError(error);
392
393
  }
393
394
  if (!res.ok) throw applyError(await httpError(res));
394
- return { ...(await res.json()), ...extras };
395
+ const applied = await res.json();
396
+ // PS-22: which theme received the files. A current server names it on the apply; an older one only in the plan
397
+ // (null for "a new draft"); oldest of all, the CLI draft looked up before the push is the one reused.
398
+ // A server whose plan carries no `newDraft` created one exactly when a draft push found no CLI draft to reuse.
399
+ const newDraft = typeof plan.newDraft === "boolean" ? plan.newDraft : Boolean(draft && !instance && cliDraft == null);
400
+ const targetInstance = applied.targetInstance ?? plan.targetInstance ?? (newDraft ? null : probeInstance) ?? null;
401
+ const targetName = newDraft ? name : cliDraft && cliDraft.id === targetInstance ? cliDraft.name ?? null : null;
402
+ return { ...applied, ...extras, targetInstance, newDraft, targetName };
403
+ }
404
+
405
+ /**
406
+ * The CLI draft a draft push writes into: the first `source: "import"` draft — `/api/dev/site` lists drafts by id,
407
+ * the server's `ensureDraftInstance` reuses the first non-live import instance by id. `null` = a push would create one.
408
+ */
409
+ export function findCliDraft(status) {
410
+ return (status?.drafts ?? []).find((d) => d.source === "import") ?? null;
395
411
  }
396
412
 
397
413
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Develop your Blocofy theme locally against live data with instant preview — login, theme dev, pull, push.",
5
5
  "type": "module",
6
6
  "license": "MIT",