@blocofy/cli 0.2.3 → 0.4.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
@@ -17,13 +17,29 @@ blocofy login [--url <url>] [--token <bcf_…>]
17
17
  # Save your platform URL + dev token (~/.blocofy/credentials.json, 0600).
18
18
  blocofy theme dev [dir] # Local dev server. Prints 3 views — local preview, a live-domain
19
19
  # preview link, and the theme editor — all auto-reloading on save.
20
- # --port <n> (default 3030), --no-sync (local preview only)
20
+ # --port <n> (default 3030), --no-sync (local preview only),
21
+ # --name <name> (name the draft when first created)
21
22
  blocofy theme pull [dir] # Download the live theme to disk.
22
- blocofy theme push [dir] # Write the local theme to the live site (create/update; no delete).
23
+ blocofy theme push [dir] # Write the local theme to a DRAFT by default (create/update; no
24
+ # delete) — preview & publish from the admin panel. --live writes
25
+ # to the live site immediately (asks to confirm; add --yes for CI).
26
+ # --name <name> names the new draft (draft mode only).
27
+ blocofy theme rename <handle> <new name>
28
+ # Rename a theme (label only). Works on any theme, live included.
29
+ # Handle from the panel theme card or `blocofy status`.
23
30
  blocofy --version
24
31
  blocofy --help
25
32
  ```
26
33
 
34
+ ## Changelog
35
+
36
+ - **0.4.0** — `theme push` / `theme dev` accept `--name <name>` to name a new draft (applied
37
+ only when the draft is first created; reuse ignores it). New `theme rename <handle> <new name>`
38
+ renames a theme (the name is a label; works on any theme, including the live one).
39
+ - **0.3.0** — `theme push` now writes to a **draft** by default; use `--live` for the old
40
+ immediate-live behavior (with a confirmation prompt; `--yes` to skip it in CI). `blocofy
41
+ status` now names the exact pages at 404 risk when a theme's pages are split off the live theme.
42
+
27
43
  ## How it works
28
44
 
29
45
  `theme dev` starts a local HTTP server. For each page request it reads your local theme
@@ -53,6 +69,7 @@ A theme is a directory of files grouped by top-level folder:
53
69
  | `asset/` | CSS and static files (`theme.css`) |
54
70
  | `pages/<slug>.json` | Page content (`/` → `pages/index.json`) |
55
71
  | `config/settings.json` | Theme settings + color schemes |
72
+ | `config/settings_schema.json` | Theme settings panel schema (synced with `pull`/`push`) |
56
73
 
57
74
  Liquid templates use the `.liquid` extension; files under `asset/` are served as-is.
58
75
 
package/bin/blocofy.mjs CHANGED
@@ -18,9 +18,9 @@ import { pullContent, pushContent } from "../lib/content-sync.mjs";
18
18
  import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
19
19
  import { startDevServer } from "../lib/dev-server.mjs";
20
20
  import { readLocalTemplates } from "../lib/local-theme.mjs";
21
- import { githubNote, statusLine, syncScopeNote } from "../lib/messages.mjs";
22
- import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
23
- import { isAffirmative, livePushDecision } from "../lib/confirm.mjs";
21
+ import { githubNote, retryNotice, statusLine, syncScopeNote } from "../lib/messages.mjs";
22
+ import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme, renameInstance } from "../lib/theme-sync.mjs";
23
+ import { isAffirmative, livePushDecision, resolvePushMode } from "../lib/confirm.mjs";
24
24
  import { hyperlink, openUrl } from "../lib/term.mjs";
25
25
  import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
26
26
 
@@ -43,13 +43,14 @@ Usage
43
43
  Save your platform URL + dev token to ~/.blocofy/credentials.json.
44
44
  Get a token from the admin panel → Settings → Theme CLI tokens.
45
45
 
46
- blocofy theme dev [dir] [--port <n>] [--no-sync]
46
+ blocofy theme dev [dir] [--port <n>] [--no-sync] [--name <name>]
47
47
  Start a dev server and print 3 auto-reloading views — Local, live-domain
48
48
  Preview, and the theme Editor. Press l / p / e to open each, q to quit.
49
49
  Edit a file and save → every open view reloads. Saves sync to a DRAFT theme
50
50
  only (never the live site). (dir defaults to cwd)
51
51
  --port <n> local port (default 3030)
52
52
  --no-sync local preview only (skip draft sync + remote views)
53
+ --name <name> name the draft when it is first created (ignored if it already exists)
53
54
 
54
55
  blocofy theme pull [dir] [--draft] [--instance <handle>]
55
56
  Download the live theme to disk. (dir defaults to cwd)
@@ -57,14 +58,21 @@ Usage
57
58
  --instance <handle> pull a specific theme by its handle (from the admin
58
59
  panel theme card, or \`blocofy status\`)
59
60
 
60
- blocofy theme push [dir] [--draft] [--yes] [--instance <handle>]
61
- Write the local theme to the LIVE site IMMEDIATELY (create/update; no delete,
62
- no preview). Asks for confirmation first; non-interactive shells must pass --yes.
63
- --draft write to a draft theme instead — preview & publish it from the
64
- admin panel (recommended; never touches the live site)
65
- --yes confirm the live push without prompting (for CI / agents)
61
+ blocofy theme push [dir] [--live] [--yes] [--instance <handle>]
62
+ By DEFAULT writes to a DRAFT theme (create/update; no delete) — preview & publish
63
+ it from the admin panel, never touching the live site. Publish it with
64
+ 'blocofy theme publish'.
65
+ --live write to the LIVE site IMMEDIATELY (no preview). Asks for
66
+ confirmation first; non-interactive shells must add --yes.
67
+ --draft explicit draft (same as the default; safe)
68
+ --yes confirm a --live push without prompting (for CI / agents)
66
69
  --instance <handle> push to a specific theme by its handle (safe targeted
67
70
  write — no live-confirmation prompt)
71
+ --name <name> name the NEW draft (draft mode only; ignored on --live/--instance)
72
+
73
+ blocofy theme rename <handle> <new name>
74
+ Rename a theme (the name is just a label). Works on any of your themes,
75
+ including the live one. Handle comes from the panel theme card or 'blocofy status'.
68
76
 
69
77
  blocofy theme publish [--instance <handle>]
70
78
  Publish a draft theme to the LIVE site. With no flag, publishes the draft that
@@ -90,7 +98,7 @@ Usage
90
98
  Examples
91
99
  blocofy login --url https://store.myblocofy.com --token bcf_xxxxxxxx
92
100
  blocofy theme pull && blocofy theme dev
93
- blocofy theme push --draft && blocofy theme publish
101
+ blocofy theme push && blocofy theme publish
94
102
  blocofy status
95
103
 
96
104
  Auth: ~/.blocofy/credentials.json (from \`login\`), or BLOCOFY_URL + BLOCOFY_TOKEN env vars.
@@ -196,8 +204,17 @@ async function themePush(rest) {
196
204
  process.exit(1);
197
205
  }
198
206
  const creds = requireCreds();
199
- const draft = Boolean(flags.draft);
200
- const instance = typeof flags.instance === "string" ? flags.instance : null;
207
+ const instanceFlag = typeof flags.instance === "string" ? flags.instance : null;
208
+ const name = typeof flags.name === "string" ? flags.name : null;
209
+
210
+ // Yeni varsayılan hedef: DRAFT (güvenli). `--live` eski anında-canlı davranışını
211
+ // açıkça geri getirir; `--instance` belirli bir temayı adresler. Sadece "live"
212
+ // modu canlıya yazar ve onay gerektirir.
213
+ const { mode, instance } = resolvePushMode({
214
+ live: Boolean(flags.live),
215
+ draft: Boolean(flags.draft),
216
+ instance: instanceFlag,
217
+ });
201
218
 
202
219
  // Hedef tenant'ı çöz ve GÖSTER — site sunucuda TOKEN'dan çözülür (URL kozmetik),
203
220
  // yanlış-tenant'a yazımı görünür kılar ("klarosa sandım, ksc'ye yazdım"). whoami
@@ -209,21 +226,26 @@ async function themePush(rest) {
209
226
  /* best-effort */
210
227
  }
211
228
  const target = site ? siteLabel(site) : creds.url;
212
- if (site) {
213
- console.log(`→ ${draft ? "Pushing to a draft" : "Pushing to the LIVE theme"} of ${target}`);
229
+ if (mode === "instance") {
230
+ console.log(`→ Pushing to theme ${instance}${site ? ` of ${target}` : ""}`);
231
+ } else if (mode === "live") {
232
+ console.log(`→ Pushing to the LIVE theme of ${target}`);
233
+ } else {
234
+ console.log(`→ Pushing to a draft${site ? ` of ${target}` : ""}`);
214
235
  }
215
236
 
216
- // Canlı push (flag'siz) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
217
- // kazara canlıya basmasın diye açık onay şart (#431 L2).
237
+ // Canlı push (`--live`) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
238
+ // kazara canlıya basmasın diye açık onay şart (#431 L2). Draft/instance modu
239
+ // güvenli → otomatik onay (prompt yok).
218
240
  const decision = livePushDecision({
219
- draft: draft || Boolean(instance),
241
+ draft: mode !== "live",
220
242
  yes: Boolean(flags.yes),
221
243
  confirm: Boolean(flags.confirm),
222
244
  isTTY: Boolean(process.stdin.isTTY),
223
245
  });
224
246
  if (decision.mustAbort) {
225
- console.error(`⚠ 'theme push' writes to the LIVE theme of ${target} immediately (no preview).`);
226
- console.error(` Non-interactive shell: pass --yes to confirm, or use --draft for a safe draft.`);
247
+ console.error(`⚠ 'theme push --live' writes to the LIVE theme of ${target} immediately (no preview).`);
248
+ console.error(` Non-interactive shell: pass --live --yes to confirm, or omit --live to push to a safe draft.`);
227
249
  process.exit(1);
228
250
  }
229
251
  if (decision.needsPrompt) {
@@ -235,15 +257,40 @@ async function themePush(rest) {
235
257
  rl.close();
236
258
  }
237
259
  if (!isAffirmative(answer)) {
238
- console.error("Aborted. Use `--draft` to push to a draft, or `--yes` to confirm the live push.");
260
+ console.error("Aborted. Omit `--live` to push to a safe draft, or pass `--live --yes` to confirm.");
239
261
  process.exit(1);
240
262
  }
241
263
  }
242
264
 
243
- const result = await pushTheme({ dir, url: creds.url, token: creds.token, draft, instance });
265
+ // `--name` yalnızca YENİ taslak yaratırken (draft modu) anlamlı — canlıya/mevcut
266
+ // instance'a yazarken ad kaydedilmez, sessizce kaybolmasın diye açıkça uyar.
267
+ if (name && mode !== "draft") {
268
+ console.error("Note: --name yalnız yeni taslak yaratırken (varsayılan push) geçerli, yok sayıldı.");
269
+ }
270
+
271
+ const result = await pushTheme({
272
+ dir,
273
+ url: creds.url,
274
+ token: creds.token,
275
+ draft: mode === "draft",
276
+ instance: mode === "instance" ? instance : null,
277
+ name: mode === "draft" ? name : null,
278
+ onRetry: (info) => console.error(retryNotice(info)),
279
+ });
280
+
281
+ // Sunucu canlı-yazımı bildirdiyse (yeni alan; eski sunucuda yok) belirgin uyar.
282
+ if (result.warning === "live_write" && result.message) {
283
+ console.error(`\n⚠ ${result.message}\n`);
284
+ }
285
+
244
286
  if (result.instanceId) {
245
287
  console.log(`Pushed to theme ${result.instanceId} (${result.created} created, ${result.updated} updated).`);
246
- console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
288
+ if (mode === "draft") {
289
+ console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
290
+ console.log(`Publish it live with: blocofy theme publish`);
291
+ } else {
292
+ console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
293
+ }
247
294
  } else {
248
295
  const extra = result.skippedDeletes
249
296
  ? `, ${result.skippedDeletes} remote file(s) absent locally (not deleted)`
@@ -299,7 +346,8 @@ async function themeDev(rest) {
299
346
  let session = null;
300
347
  if (!flags["no-sync"]) {
301
348
  try {
302
- session = await fetchDevSession({ url: creds.url, token: creds.token });
349
+ const name = typeof flags.name === "string" ? flags.name : null;
350
+ session = await fetchDevSession({ url: creds.url, token: creds.token, name });
303
351
  } catch (error) {
304
352
  console.warn(
305
353
  `Warning: dev session unavailable (${error?.message ?? error}). ` +
@@ -366,6 +414,7 @@ async function themeDev(rest) {
366
414
  token: creds.token,
367
415
  port,
368
416
  syncDraft: Boolean(session),
417
+ onRetry: (info) => console.error(` ${retryNotice(info)}`),
369
418
  // Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
370
419
  // = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
371
420
  // hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
@@ -436,6 +485,20 @@ async function themePublish(rest) {
436
485
  );
437
486
  }
438
487
 
488
+ async function themeRename(rest) {
489
+ const { flags, positionals } = parseArgs(rest);
490
+ const handle = positionals[0];
491
+ const name = positionals.slice(1).join(" ") || (typeof flags.name === "string" ? flags.name : null);
492
+ if (!handle || !name) {
493
+ console.error("Usage: blocofy theme rename <handle> <new name>");
494
+ console.error(" Rename a theme (handle from the panel theme card or `blocofy status`).");
495
+ process.exit(1);
496
+ }
497
+ const creds = requireCreds();
498
+ const result = await renameInstance({ url: creds.url, token: creds.token, instance: handle, name });
499
+ console.log(`✓ Renamed to "${result.name}" (${result.id}).`);
500
+ }
501
+
439
502
  async function status() {
440
503
  const creds = requireCreds();
441
504
  const s = await fetchSiteStatus({ url: creds.url, token: creds.token });
@@ -447,12 +510,23 @@ async function status() {
447
510
  : `Live theme: none`,
448
511
  );
449
512
  console.log(`Health: ${s.health}`);
513
+ // orphan_missing_slugs yeni sunucu alanı (PR #661). Eski/deploy-edilmemiş sunucuda
514
+ // yok → null; o durumda eski (yumuşatılmış) sayaç satırına düş.
515
+ const missing = Array.isArray(s.orphan_missing_slugs) ? s.orphan_missing_slugs : null;
450
516
  if (s.health === "live_instance_empty") {
451
517
  console.error(
452
518
  ` ⚠ The live theme has no pages — the site will 404. Publish a theme with content: blocofy theme publish`,
453
519
  );
454
520
  } else if (s.health === "pages_split") {
455
- console.warn(` ⚠ ${s.orphaned_pages} page(s) live on a non-live instance (orphan).`);
521
+ if (missing && missing.length) {
522
+ console.warn(
523
+ ` ⚠ These pages are published only on a non-live theme, so visitors can't reach them (404 risk): ` +
524
+ `${missing.join(", ")}\n` +
525
+ ` Fix: publish the theme that has them — blocofy theme publish`,
526
+ );
527
+ } else {
528
+ console.warn(` ⚠ ${s.orphaned_pages} page(s) published on a non-live theme.`);
529
+ }
456
530
  }
457
531
  if (Array.isArray(s.drafts) && s.drafts.length > 0) {
458
532
  console.log(`Drafts: ${s.drafts.map((d) => `${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
@@ -491,6 +565,11 @@ if (first === "--version" || first === "-v") {
491
565
  console.error(error?.message ?? error);
492
566
  process.exit(1);
493
567
  });
568
+ } else if (first === "theme" && rest[0] === "rename") {
569
+ themeRename(rest.slice(1)).catch((error) => {
570
+ console.error(error?.message ?? error);
571
+ process.exit(1);
572
+ });
494
573
  } else if (first === "status") {
495
574
  status().catch((error) => {
496
575
  console.error(error?.message ?? error);
package/lib/args.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /** Sonraki token'ı değer olarak almayan flag'ler. */
8
- const BOOLEAN_FLAGS = new Set(["draft", "yes", "confirm", "no-sync", "dry", "help", "version"]);
8
+ const BOOLEAN_FLAGS = new Set(["draft", "live", "yes", "confirm", "no-sync", "dry", "help", "version"]);
9
9
 
10
10
  export function parseArgs(rest) {
11
11
  const flags = {};
package/lib/confirm.mjs CHANGED
@@ -15,6 +15,22 @@ export function livePushDecision({ draft, yes, confirm, isTTY }) {
15
15
  return { autoApproved: false, needsPrompt: true, mustAbort: false };
16
16
  }
17
17
 
18
+ /**
19
+ * `theme push` hedefini seç (saf, yan etkisiz). Yeni varsayılan: DRAFT (güvenli).
20
+ * `--live` eski "anında canlı" davranışını açıkça geri getirir.
21
+ * - `instance` verilmişse → { mode: "instance", instance } (açık-hedef; `--live`
22
+ * olsa bile instance KAZANIR — kullanıcı belirli bir temayı adresledi).
23
+ * - `--live` (instance yok) → { mode: "live" } (onay gerektiren tek yol).
24
+ * - aksi halde → { mode: "draft" } (varsayılan; açık `--draft` dahil).
25
+ * - `--live` + `--draft` birlikte → draft (güvenli olan kazanır).
26
+ */
27
+ export function resolvePushMode({ live, draft, instance } = {}) {
28
+ if (instance) return { mode: "instance", instance };
29
+ if (draft) return { mode: "draft" };
30
+ if (live) return { mode: "live" };
31
+ return { mode: "draft" };
32
+ }
33
+
18
34
  /** y/N yanıtını yorumla (yes/y → onay). */
19
35
  export function isAffirmative(answer) {
20
36
  const a = String(answer ?? "").trim().toLowerCase();
@@ -20,7 +20,7 @@ import { pushTheme } from "./theme-sync.mjs";
20
20
  * separate request); direct `/asset/*` references are served from disk. Images/JS
21
21
  * are remote (Directus/CDN), so no local serving is needed for them.
22
22
  */
23
- export function startDevServer({ dir, url, token, port, syncDraft = false, onError, onReload }) {
23
+ export function startDevServer({ dir, url, token, port, syncDraft = false, onError, onReload, onRetry }) {
24
24
  const base = url.replace(/\/+$/, "");
25
25
  const clients = new Set();
26
26
 
@@ -97,7 +97,7 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
97
97
  let error = null;
98
98
  if (syncDraft) {
99
99
  try {
100
- await pushTheme({ dir, url, token, draft: true });
100
+ await pushTheme({ dir, url, token, draft: true, onRetry });
101
101
  } catch (err) {
102
102
  error = err?.message ?? String(err); // senkron hatası reload'u engellemesin
103
103
  }
@@ -108,7 +108,7 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
108
108
 
109
109
  // Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
110
110
  if (syncDraft) {
111
- void pushTheme({ dir, url, token, draft: true }).catch(() => {});
111
+ void pushTheme({ dir, url, token, draft: true, onRetry }).catch(() => {});
112
112
  }
113
113
 
114
114
  server.listen(port);
@@ -31,7 +31,10 @@ export function readLocalTemplates(dir) {
31
31
  }
32
32
  const rel = relative(dir, full).split(sep).join("/");
33
33
  const top = rel.split("/")[0] ?? "";
34
- if (!THEME_DIRS.has(top)) continue;
34
+ // config/ dizininden YALNIZ settings_schema.json senkronlanır (tema-ayarları
35
+ // şeması); color_schemes.json/theme.json kendi sistemlerinde. Platform tarafıyla
36
+ // (github-sync mapRepoPath / theme-write themeWriteKind) simetrik tek-path istisnası.
37
+ if (rel !== "config/settings_schema.json" && !THEME_DIRS.has(top)) continue;
35
38
  out[rel.endsWith(".liquid") ? rel.slice(0, -7) : rel] = readFileSync(full, "utf8");
36
39
  }
37
40
  };
package/lib/messages.mjs CHANGED
@@ -14,8 +14,8 @@ import { THEME_DIRS } from "./local-theme.mjs";
14
14
  export function syncScopeNote() {
15
15
  const synced = [...THEME_DIRS].sort().map((d) => `${d}/`).join(" ");
16
16
  return [
17
- `Synced (theme code): ${synced}`,
18
- `Not synced (cloud): config/ pages/ — edit these in the admin editor`,
17
+ `Synced (theme code): ${synced} config/settings_schema.json`,
18
+ `Not synced (cloud): pages/ (other config/ files) — edit these in the admin editor`,
19
19
  ];
20
20
  }
21
21
 
@@ -31,6 +31,16 @@ export function statusLine(session) {
31
31
  return `Local files → Draft theme ${draft} · Live theme → ${live}`;
32
32
  }
33
33
 
34
+ /**
35
+ * Geçici ağ hatasında yeniden-deneme bildirimi (saf → test edilebilir). `push`
36
+ * (ve `theme dev` taslak senkronu) sessizce retry ETMESİN; stderr'e kısa bir satır
37
+ * yazılır. `onRetry({ attempt, retries, reason })` payload'ından string üretir.
38
+ */
39
+ export function retryNotice({ attempt, retries, reason }) {
40
+ const why = reason ? ` (${reason})` : "";
41
+ return `Network error${why} — retrying (${attempt}/${retries})…`;
42
+ }
43
+
34
44
  /**
35
45
  * Dev oturumuna göre "düzlem" uyarısı döndürür (string), oturum yoksa null.
36
46
  * - GitHub bağlı → repo@branch + `git pull` ipucu.
@@ -18,6 +18,49 @@ async function errorText(res) {
18
18
  }
19
19
  }
20
20
 
21
+ /** Backoff delays (ms) between attempts — index 0 = after the first failure. */
22
+ const RETRY_BACKOFF_MS = [300, 900];
23
+
24
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
25
+
26
+ /**
27
+ * fetch wrapper that retries ONLY on transient failures: a thrown fetch (network
28
+ * error / connection reset) or a 5xx / 429 response. Permanent 4xx (401/403/422 …)
29
+ * fail immediately — no retry. Max 2 retries with a short backoff (300ms, 900ms).
30
+ *
31
+ * Safe because every endpoint here is idempotent: theme push is a file upsert
32
+ * (create/update, no delete), pull/whoami/session/site are reads, publish sets a
33
+ * pointer. Re-sending the same request cannot double-apply anything.
34
+ *
35
+ * `onRetry({ attempt, retries, reason })` is called before each wait so callers can
36
+ * surface a "Network error, retrying…" line (not silent).
37
+ */
38
+ export async function fetchWithRetry(input, init, { retries = 2, onRetry, backoff = RETRY_BACKOFF_MS } = {}) {
39
+ let lastErr;
40
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
41
+ let res;
42
+ try {
43
+ res = await fetch(input, init);
44
+ } catch (err) {
45
+ // Thrown fetch = network-level failure (DNS, reset, "fetch failed"). Transient.
46
+ lastErr = err;
47
+ if (attempt === retries) throw err;
48
+ onRetry?.({ attempt: attempt + 1, retries, reason: err?.message ?? String(err) });
49
+ await sleep(backoff[attempt] ?? 0);
50
+ continue;
51
+ }
52
+ // 5xx / 429 = transient server-side; retry. Other statuses (incl. 4xx) return as-is.
53
+ if ((res.status >= 500 || res.status === 429) && attempt < retries) {
54
+ onRetry?.({ attempt: attempt + 1, retries, reason: `HTTP ${res.status}` });
55
+ await sleep(backoff[attempt] ?? 0);
56
+ continue;
57
+ }
58
+ return res;
59
+ }
60
+ // Unreachable (loop returns or throws), but keeps the type honest.
61
+ throw lastErr;
62
+ }
63
+
21
64
  /**
22
65
  * Download a theme to disk. `{ path: content }` (stripped) → `.liquid` files.
23
66
  * With `draft`, pulls the "CLI Draft" instance (what `theme dev` syncs into)
@@ -60,9 +103,10 @@ export async function fetchWhoami({ url, token }) {
60
103
  * Dev session bilgisi (#119 `theme dev`): platform draft instance'ı hazırlar ve
61
104
  * 3 görünümün URL'lerini döner — `{ draftInstanceId, previewUrl, editorUrl, site }`.
62
105
  */
63
- export async function fetchDevSession({ url, token }) {
106
+ export async function fetchDevSession({ url, token, name = null }) {
64
107
  const base = url.replace(/\/+$/, "");
65
- const res = await fetch(`${base}/api/dev/session`, {
108
+ const query = name ? `?name=${encodeURIComponent(name)}` : "";
109
+ const res = await fetch(`${base}/api/dev/session${query}`, {
66
110
  headers: { authorization: `Bearer ${token}` },
67
111
  });
68
112
  if (!res.ok) throw new Error(await errorText(res));
@@ -74,15 +118,19 @@ export async function fetchDevSession({ url, token }) {
74
118
  * writes to a draft theme instance instead of the live theme — preview & publish
75
119
  * it from the admin panel without affecting the live site.
76
120
  */
77
- export async function pushTheme({ dir, url, token, draft = false, instance = null }) {
121
+ export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, onRetry = null }) {
78
122
  const base = url.replace(/\/+$/, "");
79
123
  const files = readLocalTemplates(dir);
80
- const payload = instance ? { files, instance } : { files, draft };
81
- const res = await fetch(`${base}/api/dev/theme`, {
82
- method: "POST",
83
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
84
- body: JSON.stringify(payload),
85
- });
124
+ const payload = instance ? { files, instance } : name ? { files, draft, name } : { files, draft };
125
+ const res = await fetchWithRetry(
126
+ `${base}/api/dev/theme`,
127
+ {
128
+ method: "POST",
129
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
130
+ body: JSON.stringify(payload),
131
+ },
132
+ { onRetry },
133
+ );
86
134
  if (!res.ok) throw new Error(await errorText(res));
87
135
  return res.json();
88
136
  }
@@ -103,6 +151,22 @@ export async function publishInstance({ url, token, instanceId }) {
103
151
  return res.json();
104
152
  }
105
153
 
154
+ /**
155
+ * Bir tema instance'ının adını değiştir (`POST /api/dev/theme/rename`). Ad yalnızca
156
+ * bir etiket — canlı instance dahil sahip olunan her instance yeniden adlandırılabilir.
157
+ * `{ ok, id, name }` döner.
158
+ */
159
+ export async function renameInstance({ url, token, instance, name }) {
160
+ const base = url.replace(/\/+$/, "");
161
+ const res = await fetch(`${base}/api/dev/theme/rename`, {
162
+ method: "POST",
163
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
164
+ body: JSON.stringify({ instance, name }),
165
+ });
166
+ if (!res.ok) throw new Error(await errorText(res));
167
+ return res.json();
168
+ }
169
+
106
170
  /**
107
171
  * Site sağlık/durum özeti (`GET /api/dev/site`) — `blocofy status`. Canlı tema instance'ı,
108
172
  * instance-başına sayfa dağılımı, taslaklar ve health döner.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.2.3",
3
+ "version": "0.4.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",