@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 +29 -11
- package/bin/blocofy.mjs +26 -12
- package/lib/target.mjs +5 -2
- package/lib/theme-sync.mjs +19 -3
- package/package.json +1 -1
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
967
|
-
//
|
|
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
|
|
971
|
-
|
|
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(
|
|
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
|
|
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
|
}
|
package/lib/theme-sync.mjs
CHANGED
|
@@ -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
|
-
|
|
327
|
-
probeInstance =
|
|
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
|
-
|
|
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