@blocofy/cli 0.1.11 → 0.2.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/bin/blocofy.mjs CHANGED
@@ -12,34 +12,24 @@ import { existsSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
  import { createInterface } from "node:readline/promises";
14
14
 
15
+ import { parseArgs } from "../lib/args.mjs";
15
16
  import { pullContent, pushContent } from "../lib/content-sync.mjs";
16
17
  import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
17
18
  import { startDevServer } from "../lib/dev-server.mjs";
18
19
  import { readLocalTemplates } from "../lib/local-theme.mjs";
19
20
  import { githubNote, statusLine, syncScopeNote } from "../lib/messages.mjs";
20
- import { fetchDevSession, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
21
+ import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
22
+ import { isAffirmative, livePushDecision } from "../lib/confirm.mjs";
21
23
  import { hyperlink, openUrl } from "../lib/term.mjs";
22
24
  import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
23
25
 
24
- const VERSION = "0.1.11";
26
+ const VERSION = "0.1.14";
25
27
  const args = process.argv.slice(2);
26
28
 
27
- /** Parse `--key value` and `--flag` (boolean) arguments. */
28
- function parseFlags(rest) {
29
- const flags = {};
30
- for (let i = 0; i < rest.length; i++) {
31
- const arg = rest[i];
32
- if (!arg.startsWith("--")) continue;
33
- const key = arg.slice(2);
34
- const next = rest[i + 1];
35
- if (next && !next.startsWith("--")) {
36
- flags[key] = next;
37
- i++;
38
- } else {
39
- flags[key] = true;
40
- }
41
- }
42
- return flags;
29
+ /** Human label for a resolved site: "Name (slug)" or just the slug. */
30
+ function siteLabel(site) {
31
+ if (!site) return "";
32
+ return site.name ? `${site.name} (${site.slug})` : site.slug;
43
33
  }
44
34
 
45
35
  function printHelp() {
@@ -55,17 +45,35 @@ Usage
55
45
  blocofy theme dev [dir] [--port <n>] [--no-sync]
56
46
  Start a dev server and print 3 auto-reloading views — Local, live-domain
57
47
  Preview, and the theme Editor. Press l / p / e to open each, q to quit.
58
- Edit a file and save → every open view reloads. (dir defaults to cwd)
48
+ Edit a file and save → every open view reloads. Saves sync to a DRAFT theme
49
+ only (never the live site). (dir defaults to cwd)
59
50
  --port <n> local port (default 3030)
60
51
  --no-sync local preview only (skip draft sync + remote views)
61
52
 
62
53
  blocofy theme pull [dir] [--draft]
63
54
  Download the live theme to disk. (dir defaults to cwd)
64
55
  --draft pull the draft theme (what 'theme dev' syncs into) instead of live
65
-
66
- blocofy theme push [dir] [--draft]
67
- Write the local theme to the site (create/update; no delete).
68
- --draft write to a draft theme — preview & publish it from the admin panel
56
+ --instance <handle> pull a specific theme by its handle (from the admin
57
+ panel theme card, or \`blocofy status\`)
58
+
59
+ blocofy theme push [dir] [--draft] [--yes]
60
+ Write the local theme to the LIVE site IMMEDIATELY (create/update; no delete,
61
+ no preview). Asks for confirmation first; non-interactive shells must pass --yes.
62
+ --draft write to a draft theme instead — preview & publish it from the
63
+ admin panel (recommended; never touches the live site)
64
+ --yes confirm the live push without prompting (for CI / agents)
65
+ --instance <handle> push to a specific theme by its handle (safe targeted
66
+ write — no live-confirmation prompt)
67
+
68
+ blocofy theme publish [--instance <handle>]
69
+ Publish a draft theme to the LIVE site. With no flag, publishes the draft that
70
+ 'theme dev' / 'theme push --draft' writes into. The server refuses to publish a
71
+ theme that has no pages (it would 404) — so publishing is always safe.
72
+ --instance <handle> publish a specific theme (handle from the panel / status)
73
+
74
+ blocofy status
75
+ Show the live theme, page distribution per instance, drafts, and a health flag
76
+ (ok / live_instance_empty / pages_split). Run before/after publishing.
69
77
 
70
78
  blocofy pages pull [dir] / pages push [dir]
71
79
  pull: download published pages → pages/<slug>.json.
@@ -81,7 +89,8 @@ Usage
81
89
  Examples
82
90
  blocofy login --url https://store.myblocofy.com --token bcf_xxxxxxxx
83
91
  blocofy theme pull && blocofy theme dev
84
- blocofy theme push --draft
92
+ blocofy theme push --draft && blocofy theme publish
93
+ blocofy status
85
94
 
86
95
  Auth: ~/.blocofy/credentials.json (from \`login\`), or BLOCOFY_URL + BLOCOFY_TOKEN env vars.
87
96
  The CLI does not build assets — bring your own (npm/Vite/Tailwind); the platform serves
@@ -100,7 +109,7 @@ async function promptValid(rl, question, normalize, valid, hint, tries = 3) {
100
109
  }
101
110
 
102
111
  async function login(rest) {
103
- const flags = parseFlags(rest);
112
+ const { flags } = parseArgs(rest);
104
113
  let url = typeof flags.url === "string" ? normalizeUrl(flags.url) : "";
105
114
  let token = typeof flags.token === "string" ? flags.token.trim() : "";
106
115
 
@@ -144,6 +153,16 @@ async function login(rest) {
144
153
 
145
154
  saveCredentials({ url, token });
146
155
  console.log(`✓ Saved credentials → ${credentialsPath()}`);
156
+
157
+ // Token'ın GERÇEK site'ını göster — site sunucuda TOKEN'dan çözülür, URL kozmetik.
158
+ // Yanlış tenant'ın token'ıyla login olduysan ("klarosa URL + ksc token") hemen
159
+ // görürsün. whoami yoksa/erişilemezse sessiz geç.
160
+ try {
161
+ const who = await fetchWhoami({ url, token });
162
+ if (who?.site) console.log(` Site: ${siteLabel(who.site)} — your theme commands target this tenant.`);
163
+ } catch {
164
+ /* best-effort */
165
+ }
147
166
  console.log(`Next: cd into your theme directory, then run blocofy theme dev`);
148
167
  }
149
168
 
@@ -158,28 +177,71 @@ function requireCreds() {
158
177
  }
159
178
 
160
179
  async function themePull(rest) {
161
- const flags = parseFlags(rest);
162
- const positional = rest.filter((a) => !a.startsWith("--"));
163
- const dir = resolve(positional[0] ?? process.cwd());
180
+ const { flags, positionals } = parseArgs(rest);
181
+ const dir = resolve(positionals[0] ?? process.cwd());
164
182
  const creds = requireCreds();
165
183
  const draft = Boolean(flags.draft);
166
- const { count } = await pullTheme({ dir, url: creds.url, token: creds.token, draft });
167
- console.log(`Downloaded ${count} ${draft ? "draft" : "live"} theme files → ${dir}`);
184
+ const instance = typeof flags.instance === "string" ? flags.instance : null;
185
+ const { count } = await pullTheme({ dir, url: creds.url, token: creds.token, draft, instance });
186
+ const what = instance ? `instance ${instance}` : draft ? "draft" : "live";
187
+ console.log(`Downloaded ${count} ${what} theme files → ${dir}`);
168
188
  }
169
189
 
170
190
  async function themePush(rest) {
171
- const flags = parseFlags(rest);
172
- const positional = rest.filter((a) => !a.startsWith("--"));
173
- const dir = resolve(positional[0] ?? process.cwd());
191
+ const { flags, positionals } = parseArgs(rest);
192
+ const dir = resolve(positionals[0] ?? process.cwd());
174
193
  if (!existsSync(dir)) {
175
194
  console.error(`Theme directory not found: ${dir}`);
176
195
  process.exit(1);
177
196
  }
178
197
  const creds = requireCreds();
179
198
  const draft = Boolean(flags.draft);
180
- const result = await pushTheme({ dir, url: creds.url, token: creds.token, draft });
181
- if (result.draft) {
182
- console.log(`Pushed to draft theme #${result.instanceId} (${result.created} created, ${result.updated} updated).`);
199
+ const instance = typeof flags.instance === "string" ? flags.instance : null;
200
+
201
+ // Hedef tenant'ı çöz ve GÖSTER — site sunucuda TOKEN'dan çözülür (URL kozmetik),
202
+ // yanlış-tenant'a yazımı görünür kılar ("klarosa sandım, ksc'ye yazdım"). whoami
203
+ // yoksa/erişilemezse sessiz geç; etiketi confirmation mesajlarında da kullan.
204
+ let site = null;
205
+ try {
206
+ site = (await fetchWhoami({ url: creds.url, token: creds.token })).site;
207
+ } catch {
208
+ /* best-effort */
209
+ }
210
+ const target = site ? siteLabel(site) : creds.url;
211
+ if (site) {
212
+ console.log(`→ ${draft ? "Pushing to a draft" : "Pushing to the LIVE theme"} of ${target}`);
213
+ }
214
+
215
+ // Canlı push (flag'siz) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
216
+ // kazara canlıya basmasın diye açık onay şart (#431 L2).
217
+ const decision = livePushDecision({
218
+ draft: draft || Boolean(instance),
219
+ yes: Boolean(flags.yes),
220
+ confirm: Boolean(flags.confirm),
221
+ isTTY: Boolean(process.stdin.isTTY),
222
+ });
223
+ if (decision.mustAbort) {
224
+ console.error(`⚠ 'theme push' writes to the LIVE theme of ${target} immediately (no preview).`);
225
+ console.error(` Non-interactive shell: pass --yes to confirm, or use --draft for a safe draft.`);
226
+ process.exit(1);
227
+ }
228
+ if (decision.needsPrompt) {
229
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
230
+ let answer;
231
+ try {
232
+ answer = await rl.question(`⚠ Push to the LIVE theme of ${target}? Immediate, no preview. [y/N] `);
233
+ } finally {
234
+ rl.close();
235
+ }
236
+ if (!isAffirmative(answer)) {
237
+ console.error("Aborted. Use `--draft` to push to a draft, or `--yes` to confirm the live push.");
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ const result = await pushTheme({ dir, url: creds.url, token: creds.token, draft, instance });
243
+ if (result.instanceId) {
244
+ console.log(`Pushed to theme ${result.instanceId} (${result.created} created, ${result.updated} updated).`);
183
245
  console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
184
246
  } else {
185
247
  const extra = result.skippedDeletes
@@ -190,8 +252,8 @@ async function themePush(rest) {
190
252
  }
191
253
 
192
254
  async function contentPush(scope, rest) {
193
- const positional = rest.filter((a) => !a.startsWith("--"));
194
- const dir = resolve(positional[0] ?? process.cwd());
255
+ const { positionals } = parseArgs(rest);
256
+ const dir = resolve(positionals[0] ?? process.cwd());
195
257
  if (!existsSync(dir)) {
196
258
  console.error(`Directory not found: ${dir}`);
197
259
  process.exit(1);
@@ -212,17 +274,16 @@ async function contentPush(scope, rest) {
212
274
  }
213
275
 
214
276
  async function contentPull(scope, rest) {
215
- const positional = rest.filter((a) => !a.startsWith("--"));
216
- const dir = resolve(positional[0] ?? process.cwd());
277
+ const { positionals } = parseArgs(rest);
278
+ const dir = resolve(positionals[0] ?? process.cwd());
217
279
  const creds = requireCreds();
218
280
  const { count } = await pullContent({ dir, url: creds.url, token: creds.token, scope });
219
281
  console.log(`Downloaded ${count} ${scope === "settings" ? "settings" : "page"} file(s) → ${dir}`);
220
282
  }
221
283
 
222
284
  async function themeDev(rest) {
223
- const flags = parseFlags(rest);
224
- const positional = rest.filter((a) => !a.startsWith("--"));
225
- const themeDir = resolve(positional[0] ?? process.cwd());
285
+ const { flags, positionals } = parseArgs(rest);
286
+ const themeDir = resolve(positionals[0] ?? process.cwd());
226
287
 
227
288
  if (!existsSync(themeDir)) {
228
289
  console.error(`Theme directory not found: ${themeDir}`);
@@ -250,7 +311,9 @@ async function themeDev(rest) {
250
311
  const previewUrl = session ? `${session.previewUrl}&hr=${port}` : null;
251
312
  const editorUrl = session ? `${session.editorUrl}&hr=${port}` : null;
252
313
 
253
- console.log(`\nblocofy theme dev — ${creds.url} (token ${creds.token.slice(0, 8)}…)\n`);
314
+ // Çözülen site'ı göster (session TOKEN'dan çözer) → hangi tenant'ı düzenlediğin belli olsun.
315
+ const siteLine = session?.site ? `${siteLabel(session.site)} · ` : "";
316
+ console.log(`\nblocofy theme dev — ${siteLine}${creds.url} (token ${creds.token.slice(0, 8)}…)\n`);
254
317
  console.log(` (l) Local ${hyperlink(localUrl)}`);
255
318
  if (previewUrl) console.log(` (p) Preview ${hyperlink(previewUrl)}`);
256
319
  if (editorUrl) console.log(` (e) Editor ${hyperlink(editorUrl)}`);
@@ -357,6 +420,45 @@ async function themeDev(rest) {
357
420
  }
358
421
  }
359
422
 
423
+ async function themePublish(rest) {
424
+ const { flags } = parseArgs(rest);
425
+ const creds = requireCreds();
426
+ let instance = typeof flags.instance === "string" ? flags.instance : null;
427
+ if (!instance) {
428
+ // Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla.
429
+ const session = await fetchDevSession({ url: creds.url, token: creds.token });
430
+ instance = session.draftInstanceId;
431
+ }
432
+ const result = await publishInstance({ url: creds.url, token: creds.token, instanceId: instance });
433
+ console.log(
434
+ `✓ Theme ${result.published} is now LIVE${result.cloned ? " (pages cloned from the previous live theme)" : ""}.`,
435
+ );
436
+ }
437
+
438
+ async function status() {
439
+ const creds = requireCreds();
440
+ const s = await fetchSiteStatus({ url: creds.url, token: creds.token });
441
+ const live = s.live_theme_instance;
442
+ console.log(`\nSite: ${s.site.slug} · ${s.site.id}`);
443
+ console.log(
444
+ live
445
+ ? `Live theme: ${live.id}${live.name ? ` ${live.name}` : ""} — ${live.template_count} files, ${s.pages_on_live} pages`
446
+ : `Live theme: none`,
447
+ );
448
+ console.log(`Health: ${s.health}`);
449
+ if (s.health === "live_instance_empty") {
450
+ console.error(
451
+ ` ⚠ The live theme has no pages — the site will 404. Publish a theme with content: blocofy theme publish`,
452
+ );
453
+ } else if (s.health === "pages_split") {
454
+ console.warn(` ⚠ ${s.orphaned_pages} page(s) live on a non-live instance (orphan).`);
455
+ }
456
+ if (Array.isArray(s.drafts) && s.drafts.length > 0) {
457
+ console.log(`Drafts: ${s.drafts.map((d) => `${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
458
+ }
459
+ console.log("");
460
+ }
461
+
360
462
  const [first, ...rest] = args;
361
463
 
362
464
  if (first === "--version" || first === "-v") {
@@ -383,6 +485,16 @@ if (first === "--version" || first === "-v") {
383
485
  console.error(error?.message ?? error);
384
486
  process.exit(1);
385
487
  });
488
+ } else if (first === "theme" && rest[0] === "publish") {
489
+ themePublish(rest.slice(1)).catch((error) => {
490
+ console.error(error?.message ?? error);
491
+ process.exit(1);
492
+ });
493
+ } else if (first === "status") {
494
+ status().catch((error) => {
495
+ console.error(error?.message ?? error);
496
+ process.exit(1);
497
+ });
386
498
  } else if (first === "pages" && rest[0] === "pull") {
387
499
  contentPull("pages", rest.slice(1)).catch((error) => {
388
500
  console.error(error?.message ?? error);
package/lib/args.mjs ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Birleşik arg tokenizer. Tek geçişte hem flag'leri hem positional'ları çıkarır —
3
+ * böylece bir flag DEĞERİ (örn. `--port 3035`'in `3035`'i) yanlışlıkla positional
4
+ * (tema dizini) sanılmaz. Boolean flag'ler sonraki token'ı YUTMAZ.
5
+ */
6
+
7
+ /** Sonraki token'ı değer olarak almayan flag'ler. */
8
+ const BOOLEAN_FLAGS = new Set(["draft", "yes", "confirm", "no-sync", "dry", "help", "version"]);
9
+
10
+ export function parseArgs(rest) {
11
+ const flags = {};
12
+ const positionals = [];
13
+ for (let i = 0; i < rest.length; i++) {
14
+ const arg = rest[i];
15
+ if (!arg.startsWith("--")) {
16
+ positionals.push(arg);
17
+ continue;
18
+ }
19
+ const key = arg.slice(2);
20
+ if (BOOLEAN_FLAGS.has(key)) {
21
+ flags[key] = true;
22
+ continue;
23
+ }
24
+ const next = rest[i + 1];
25
+ if (next !== undefined && !next.startsWith("--")) {
26
+ flags[key] = next;
27
+ i++;
28
+ } else {
29
+ flags[key] = true;
30
+ }
31
+ }
32
+ return { flags, positionals };
33
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canlı `theme push` onay kararı (#431 L2). `theme push` (flag'siz) site'in CANLI
3
+ * temasına ANINDA yazar (önizleme yok) — bir agent/CI kazara canlı temayı bozabilir.
4
+ * Bu yüzden canlı push açık onay ister:
5
+ * - `--draft` → onay gerekmez (zaten canlıya yazmaz, taslağa yazar).
6
+ * - `--yes` / `--confirm` → açık onay (CI/agent için).
7
+ * - interaktif TTY → y/N sorulur.
8
+ * - non-TTY (script/agent) + onay yok → ABORT (sessizce canlıya basılmaz).
9
+ *
10
+ * Saf karar (yan etkisiz, test edilebilir): { autoApproved, needsPrompt, mustAbort }.
11
+ */
12
+ export function livePushDecision({ draft, yes, confirm, isTTY }) {
13
+ if (draft || yes || confirm) return { autoApproved: true, needsPrompt: false, mustAbort: false };
14
+ if (!isTTY) return { autoApproved: false, needsPrompt: false, mustAbort: true };
15
+ return { autoApproved: false, needsPrompt: true, mustAbort: false };
16
+ }
17
+
18
+ /** y/N yanıtını yorumla (yes/y → onay). */
19
+ export function isAffirmative(answer) {
20
+ const a = String(answer ?? "").trim().toLowerCase();
21
+ return a === "y" || a === "yes";
22
+ }
package/lib/messages.mjs CHANGED
@@ -26,8 +26,8 @@ export function syncScopeNote() {
26
26
  */
27
27
  export function statusLine(session) {
28
28
  if (!session) return null;
29
- const draft = session.draftInstanceId != null ? `#${session.draftInstanceId}` : "—";
30
- const live = session.liveThemeId != null ? `#${session.liveThemeId}` : "(none)";
29
+ const draft = session.draftInstanceId != null ? session.draftInstanceId : "—";
30
+ const live = session.liveThemeId != null ? session.liveThemeId : "(none)";
31
31
  return `Local files → Draft theme ${draft} · Live theme → ${live}`;
32
32
  }
33
33
 
@@ -23,9 +23,10 @@ async function errorText(res) {
23
23
  * With `draft`, pulls the "CLI Draft" instance (what `theme dev` syncs into)
24
24
  * instead of the live theme — symmetric with `push --draft`.
25
25
  */
26
- export async function pullTheme({ dir, url, token, draft = false }) {
26
+ export async function pullTheme({ dir, url, token, draft = false, instance = null }) {
27
27
  const base = url.replace(/\/+$/, "");
28
- const res = await fetch(`${base}/api/dev/theme${draft ? "?draft=1" : ""}`, {
28
+ const query = instance ? `?instance=${encodeURIComponent(instance)}` : draft ? "?draft=1" : "";
29
+ const res = await fetch(`${base}/api/dev/theme${query}`, {
29
30
  headers: { authorization: `Bearer ${token}` },
30
31
  });
31
32
  if (!res.ok) throw new Error(await errorText(res));
@@ -40,6 +41,21 @@ export async function pullTheme({ dir, url, token, draft = false }) {
40
41
  return { count };
41
42
  }
42
43
 
44
+ /**
45
+ * Token'ın GERÇEK site'ını çözer (`GET /api/dev/whoami`). Site sunucuda TOKEN'dan
46
+ * çözülür — login URL'i kozmetik. CLI bunu `login`'de (doğrula+göster) ve `push`
47
+ * öncesi (hedef tenant'ı yaz) çağırır; yanlış-tenant'a yazımı görünür kılar.
48
+ * `{ site: { id, slug, name }, liveThemeId }`.
49
+ */
50
+ export async function fetchWhoami({ url, token }) {
51
+ const base = url.replace(/\/+$/, "");
52
+ const res = await fetch(`${base}/api/dev/whoami`, {
53
+ headers: { authorization: `Bearer ${token}` },
54
+ });
55
+ if (!res.ok) throw new Error(await errorText(res));
56
+ return res.json();
57
+ }
58
+
43
59
  /**
44
60
  * Dev session bilgisi (#119 `theme dev`): platform draft instance'ı hazırlar ve
45
61
  * 3 görünümün URL'lerini döner — `{ draftInstanceId, previewUrl, editorUrl, site }`.
@@ -58,14 +74,42 @@ export async function fetchDevSession({ url, token }) {
58
74
  * writes to a draft theme instance instead of the live theme — preview & publish
59
75
  * it from the admin panel without affecting the live site.
60
76
  */
61
- export async function pushTheme({ dir, url, token, draft = false }) {
77
+ export async function pushTheme({ dir, url, token, draft = false, instance = null }) {
62
78
  const base = url.replace(/\/+$/, "");
63
79
  const files = readLocalTemplates(dir);
80
+ const payload = instance ? { files, instance } : { files, draft };
64
81
  const res = await fetch(`${base}/api/dev/theme`, {
65
82
  method: "POST",
66
83
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
67
- body: JSON.stringify({ files, draft }),
84
+ body: JSON.stringify(payload),
85
+ });
86
+ if (!res.ok) throw new Error(await errorText(res));
87
+ return res.json();
88
+ }
89
+
90
+ /**
91
+ * Bir taslak tema instance'ını CANLIYA al (`POST /api/dev/publish`). Sunucu guard'ı
92
+ * içi-sayfasız bir instance'ı reddeder ya da canlının sayfalarını klonlar (#431) —
93
+ * yayın sonrası site asla 404'e düşmez. `{ ok, published, cloned }` döner.
94
+ */
95
+ export async function publishInstance({ url, token, instanceId }) {
96
+ const base = url.replace(/\/+$/, "");
97
+ const res = await fetch(`${base}/api/dev/publish`, {
98
+ method: "POST",
99
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
100
+ body: JSON.stringify({ instanceId }),
68
101
  });
69
102
  if (!res.ok) throw new Error(await errorText(res));
70
103
  return res.json();
71
104
  }
105
+
106
+ /**
107
+ * Site sağlık/durum özeti (`GET /api/dev/site`) — `blocofy status`. Canlı tema instance'ı,
108
+ * instance-başına sayfa dağılımı, taslaklar ve health döner.
109
+ */
110
+ export async function fetchSiteStatus({ url, token }) {
111
+ const base = url.replace(/\/+$/, "");
112
+ const res = await fetch(`${base}/api/dev/site`, { headers: { authorization: `Bearer ${token}` } });
113
+ if (!res.ok) throw new Error(await errorText(res));
114
+ return res.json();
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.1.11",
3
+ "version": "0.2.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",