@blocofy/cli 0.5.0 → 0.7.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
@@ -24,6 +24,8 @@ blocofy theme push [dir] # Write the local theme to a DRAFT by default (create/
24
24
  # delete) — preview & publish from the admin panel. --live writes
25
25
  # to the live site immediately (asks to confirm; add --yes for CI).
26
26
  # --name <name> names the new draft (draft mode only).
27
+ # --prune also removes target files deleted locally (lists them
28
+ # first; on the live theme asks to confirm — --yes for CI).
27
29
  blocofy theme rename <handle> <new name>
28
30
  # Rename a theme (label only). Works on any theme, live included.
29
31
  # Handle from the panel theme card or `blocofy status`.
@@ -33,6 +35,28 @@ blocofy --help
33
35
 
34
36
  ## Changelog
35
37
 
38
+ - **0.7.0** — `theme push --prune`: files that exist on the platform but no longer exist locally are
39
+ removed. Until now a push merged remote-only files back into the upload, so a file deleted locally
40
+ stayed on the theme forever. The list is printed before anything is written; pruning the LIVE theme
41
+ (`--live`, or `--instance` pointing at the live theme) asks for confirmation or `--yes`, and a
42
+ non-interactive shell without `--yes` exits without writing. Removal goes through the platform's atomic
43
+ deploy, so it is revision-tracked. A draft push also no longer creates a draft as a side effect of its
44
+ pre-push remote read: the CLI probes the existing CLI draft by handle and skips the probe when there is
45
+ none. Previously a failure during that read left an empty draft behind and printed a bare `HTTP 500`.
46
+
47
+ - **0.6.0** — `theme dev`'s local view finally shows what you are working on. It used to render your
48
+ DRAFT theme against the site's LIVE page content, so draft page documents were invisible; each render
49
+ now carries the draft theme instance and the platform resolves that instance's pages. Three failures
50
+ caused by a retired server endpoint (`/api/dev/session`, gone since the platform's preview-security
51
+ work) are fixed at the root: **draft sync** was switched off whenever that endpoint failed even though
52
+ sync never used it (it posts to `/api/dev/theme`); **`theme publish`** without `--instance` called it
53
+ with no error handling and died with an empty message — it now resolves the CLI draft from
54
+ `blocofy status` (`source: "import"`); and an empty response body produced the undiagnosable
55
+ `dev session unavailable ()` — errors now fall back to `HTTP <status>` and a 410 explains itself.
56
+ If the server ever stops returning a draft handle, the CLI now says so instead of silently rendering
57
+ live content. Remote (shared-link and editor) preview is NOT restored — that surface was retired on
58
+ the platform side; `theme dev` prints the local view only.
59
+
36
60
  - **0.5.0** — M4 canonical deploy protocol. `theme push` now deploys through the platform's atomic
37
61
  source pipeline: the CLI declares the protocol handshake and auto-generates a per-push idempotency
38
62
  key (transport retries converge; `--idempotency-key <k>` overrides it for scripting). A `--live`
package/bin/blocofy.mjs CHANGED
@@ -35,7 +35,7 @@ const args = process.argv.slice(2);
35
35
  const KNOWN = {
36
36
  login: ["url", "token"],
37
37
  themePull: ["draft", "instance"],
38
- themePush: ["diff", "draft", "instance", "name", "live", "yes", "confirm", "dry-run", "validate", "idempotency-key"],
38
+ themePush: ["diff", "draft", "instance", "name", "live", "yes", "confirm", "dry-run", "validate", "idempotency-key", "prune"],
39
39
  themeDev: ["port", "dry", "no-sync", "name"],
40
40
  themePublish: ["instance"],
41
41
  themeRename: ["name"],
@@ -81,7 +81,7 @@ Usage
81
81
  --instance <handle> pull a specific theme by its handle (from the admin
82
82
  panel theme card, or \`blocofy status\`)
83
83
 
84
- blocofy theme push [dir] [--live] [--yes] [--instance <handle>]
84
+ blocofy theme push [dir] [--live] [--yes] [--instance <handle>] [--prune]
85
85
  By DEFAULT writes to a DRAFT theme (create/update; no delete) — preview & publish
86
86
  it from the admin panel, never touching the live site. Publish it with
87
87
  'blocofy theme publish'.
@@ -96,6 +96,8 @@ Usage
96
96
  --validate alias for --dry-run (validate only, nothing written)
97
97
  --diff show what a push WOULD change vs the target (read-only), then stop
98
98
  --idempotency-key <k> attach an idempotency key so a retried push is not double-applied
99
+ --prune also REMOVE target files that no longer exist locally (lists them first;
100
+ on the live theme asks to confirm — non-interactive shells must add --yes)
99
101
 
100
102
  blocofy theme rename <handle> <new name>
101
103
  Rename a theme (the name is just a label). Works on any of your themes,
@@ -261,7 +263,7 @@ async function themePush(rest) {
261
263
  }
262
264
  for (const k of d.added) console.log(` + ${k}`);
263
265
  for (const k of d.changed) console.log(` ~ ${k}`);
264
- for (const k of d.removed) console.log(` - ${k} (present on target, absent locally — push does not delete)`);
266
+ for (const k of d.removed) console.log(` - ${k} (present on target, absent locally — ${flags.prune ? "--prune removes it" : "push does not delete"})`);
265
267
  console.log(`\n${d.added.length} added, ${d.changed.length} changed, ${d.removed.length} remote-only.`);
266
268
  return;
267
269
  }
@@ -279,8 +281,10 @@ async function themePush(rest) {
279
281
  // yanlış-tenant'a yazımı görünür kılar ("klarosa sandım, ksc'ye yazdım"). whoami
280
282
  // yoksa/erişilemezse sessiz geç; etiketi confirmation mesajlarında da kullan.
281
283
  let site = null;
284
+ let whoami = null;
282
285
  try {
283
- site = (await fetchWhoami({ url: creds.url, token: creds.token })).site;
286
+ whoami = await fetchWhoami({ url: creds.url, token: creds.token });
287
+ site = whoami.site;
284
288
  } catch {
285
289
  /* best-effort */
286
290
  }
@@ -322,6 +326,34 @@ async function themePush(rest) {
322
326
  }
323
327
  }
324
328
 
329
+ // PS-13 `--prune`: canlı temadan dosya SİLER. Hedef canlıysa (`--live` ya da canlı temanın handle'ı
330
+ // verilmiş `--instance`; whoami çözülemediyse canlı sayılır) canlı-push onay kuralı aynen uygulanır:
331
+ // `--yes`/`--confirm` → onaylı, TTY → liste basıldıktan sonra y/N, non-TTY → yazımsız çıkış.
332
+ const prune = Boolean(flags.prune) && !dryRun;
333
+ const liveTarget = mode === "live" || (mode === "instance" && (whoami == null || String(instance) === String(whoami.liveThemeId)));
334
+ const pruneDecision = livePushDecision({
335
+ draft: !prune || !liveTarget,
336
+ yes: Boolean(flags.yes),
337
+ confirm: Boolean(flags.confirm),
338
+ isTTY: Boolean(process.stdin.isTTY),
339
+ });
340
+ if (pruneDecision.mustAbort) {
341
+ console.error(`⚠ 'theme push --prune' removes files from the LIVE theme of ${target}.`);
342
+ console.error(` Non-interactive shell: pass --prune --yes to confirm. Nothing was written.`);
343
+ process.exit(1);
344
+ }
345
+ const confirmPrune = async (keys) => {
346
+ console.log(`--prune will remove ${keys.length} file(s) absent locally:`);
347
+ for (const k of keys) console.log(` - ${k}`);
348
+ if (!pruneDecision.needsPrompt) return true;
349
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
350
+ try {
351
+ return isAffirmative(await rl.question(`⚠ Remove these files from the LIVE theme of ${target}? [y/N] `));
352
+ } finally {
353
+ rl.close();
354
+ }
355
+ };
356
+
325
357
  // `--name` yalnızca YENİ taslak yaratırken (draft modu) anlamlı — canlıya/mevcut
326
358
  // instance'a yazarken ad kaydedilmez, sessizce kaybolmasın diye açıkça uyar.
327
359
  if (name && mode !== "draft") {
@@ -351,6 +383,8 @@ async function themePush(rest) {
351
383
  dryRun,
352
384
  idempotencyKey,
353
385
  onRetry: (info) => console.error(retryNotice(info)),
386
+ prune,
387
+ confirmPrune,
354
388
  });
355
389
  } catch (error) {
356
390
  if (error?.code === "cli_upgrade_required") {
@@ -367,6 +401,11 @@ async function themePush(rest) {
367
401
  throw error;
368
402
  }
369
403
 
404
+ if (result.aborted) {
405
+ console.error("Aborted. Nothing was written.");
406
+ process.exit(1);
407
+ }
408
+
370
409
  // --dry-run / --validate: the server validated without writing. Report and stop.
371
410
  if (result.dryRun) {
372
411
  const warnings = Array.isArray(result.warnings) ? result.warnings : [];
@@ -380,6 +419,9 @@ async function themePush(rest) {
380
419
  if (Array.isArray(result.remoteOnlyKept) && result.remoteOnlyKept.length) {
381
420
  console.log(` (kept ${result.remoteOnlyKept.length} remote-only file(s) — push does not delete)`);
382
421
  }
422
+ if (Array.isArray(result.remoteOnlyRemoved) && result.remoteOnlyRemoved.length) {
423
+ console.log(` (removed ${result.remoteOnlyRemoved.length} file(s) absent locally — --prune)`);
424
+ }
383
425
  console.log(`✓ Deployed atomically: deployment #${result.deploymentId}, revision #${result.sourceRevisionId}, pointer v${result.pointerVersion}.`);
384
426
  if (mode === "draft") {
385
427
  console.log("Preview & publish it in the admin panel: Theme -> Theme library -> \"Open in editor\".");
@@ -460,8 +502,8 @@ async function themeDev(rest) {
460
502
  session = await fetchDevSession({ url: creds.url, token: creds.token, name });
461
503
  } catch (error) {
462
504
  console.warn(
463
- `Warning: dev session unavailable (${error?.message ?? error}). ` +
464
- `Local preview only — live-domain/editor views + draft sync disabled.`,
505
+ `Warning: dev session unavailable (${error?.message || error}). ` +
506
+ `Live-domain and editor views are disabled; draft sync and local preview keep working.`,
465
507
  );
466
508
  }
467
509
  }
@@ -523,8 +565,12 @@ async function themeDev(rest) {
523
565
  url: creds.url,
524
566
  token: creds.token,
525
567
  port,
526
- syncDraft: Boolean(session),
568
+ // Taslak senkronu dev session'a BAĞLI DEĞİL: `pushTheme({draft:true})` /api/dev/theme'e gider ve
569
+ // session'dan hiçbir veri kullanmaz. Bu satır `Boolean(session)` iken, session 410 alınca senkron
570
+ // da sessizce kapanıyordu — ölü bir uç, çalışan bir özelliği götürüyordu. Tek kapatma yolu --no-sync.
571
+ syncDraft: !flags["no-sync"],
527
572
  onRetry: (info) => console.error(` ${retryNotice(info)}`),
573
+ onWarn: (msg) => console.warn(` ⚠ ${msg}`),
528
574
  // Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
529
575
  // = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
530
576
  // hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
@@ -585,9 +631,26 @@ async function themePublish(rest) {
585
631
  const creds = requireCreds();
586
632
  let instance = typeof flags.instance === "string" ? flags.instance : null;
587
633
  if (!instance) {
588
- // Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla.
589
- const session = await fetchDevSession({ url: creds.url, token: creds.token });
590
- instance = session.draftInstanceId;
634
+ // Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla. Kaynak
635
+ // `GET /api/dev/site` — eski `fetchDevSession` yolu sunucuda 410'a döndü ve bu komutu
636
+ // try/catch'siz, boş mesajlı bir Error ile tamamen çalışmaz hâle getirmişti.
637
+ //
638
+ // `drafts` canlı OLMAYAN HER instance'ı içerir; sunucunun `ensureDraftInstance`'ı ise
639
+ // `source === "import"` olanı seçer (yayınlanan taslak import'tan çıkarılır). Aynı seçimi
640
+ // burada tekrarla — yoksa bir kez yayın yapmış her sitede iki taslak görünür ve komut takılır.
641
+ const status = await fetchSiteStatus({ url: creds.url, token: creds.token });
642
+ const drafts = status?.drafts ?? [];
643
+ const cliDrafts = drafts.filter((d) => d.source === "import");
644
+ if (cliDrafts.length === 1) {
645
+ instance = cliDrafts[0].id;
646
+ } else if (drafts.length === 0) {
647
+ console.error("No draft theme to publish. Create one first: blocofy theme push --draft");
648
+ process.exit(1);
649
+ } else {
650
+ console.error("Could not tell which draft to publish — pick one with --instance <handle>:");
651
+ for (const d of drafts) console.error(` ${d.id} ${d.name ?? "(unnamed)"}`);
652
+ process.exit(1);
653
+ }
591
654
  }
592
655
  const result = await publishInstance({ url: creds.url, token: creds.token, instanceId: instance });
593
656
  console.log(
@@ -20,16 +20,52 @@ 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, onRetry }) {
23
+ export function startDevServer({ dir, url, token, port, syncDraft = false, onError, onReload, onRetry, onWarn }) {
24
24
  const base = url.replace(/\/+$/, "");
25
25
  const clients = new Set();
26
26
 
27
+ // Taslak instance handle'ı: `pushTheme({draft:true})` yanıtından gelir (ek istek YOK). Render
28
+ // istekleri bunu taşıyınca platform CANLI sayfa belgeleri yerine TASLAĞIN sayfa belgelerini
29
+ // çözer — `theme dev`'in yerel görünümü nihayet üzerinde çalışılan şeyi gösterir.
30
+ let draftInstance = null;
31
+ // Kanonik yazma yolu ham control-plane JSON'u döndürür ve `instanceId` içermeyebilir. O durumda
32
+ // sessizce canlıya dönmek, bu onarımın kapattığı arıza sınıfının aynısı olurdu — bir kez uyar.
33
+ let warnedNoHandle = false;
34
+ const noteHandle = (pushed) => {
35
+ if (pushed?.instanceId) {
36
+ draftInstance = pushed.instanceId;
37
+ return;
38
+ }
39
+ if (!warnedNoHandle) {
40
+ warnedNoHandle = true;
41
+ onWarn?.(
42
+ "the server did not return a draft theme handle — local preview will render LIVE page content, not your draft.",
43
+ );
44
+ }
45
+ };
46
+
47
+ // Açılıştaki push'u BİR promise olarak tut: ilk render isteği bunu bekler, yoksa ilk sayfa
48
+ // yüklemesi canlı içerikle gelir ve geliştirici "taslağım görünmüyor" sanır.
49
+ const draftReady = syncDraft
50
+ ? pushTheme({ dir, url, token, draft: true, onRetry })
51
+ .then(noteHandle)
52
+ .catch(() => {})
53
+ : Promise.resolve();
54
+
27
55
  async function renderPath(pathname) {
56
+ // Açılış push'u bitene kadar bekle — yalnız ilk istekte gerçekten bekler, sonra çözülmüş promise.
57
+ if (!draftInstance) await draftReady;
28
58
  const templates = readLocalTemplates(dir);
29
59
  const res = await fetch(`${base}/api/dev/render`, {
30
60
  method: "POST",
31
61
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
32
- body: JSON.stringify({ path: pathname, templates }),
62
+ // `preview` BİLİNÇLE gönderilmez: sunucuda preview yalnız tazelik değil — önizleme bandı,
63
+ // editör bridge'i ve blok işaretleyicileri de ona bağlı. `theme dev`'in vaadi prod paritesi.
64
+ body: JSON.stringify({
65
+ path: pathname,
66
+ templates,
67
+ ...(draftInstance ? { instance: draftInstance } : {}),
68
+ }),
33
69
  });
34
70
  const text = await res.text();
35
71
  if (!res.ok) {
@@ -100,7 +136,7 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
100
136
  // BİLİNÇLE key'siz (0.5.0): dev sync her kayıtta koşar — kanonik mühür (key'li dal) kayıt
101
137
  // başına revizyon+deployment üretirdi (retention yok) ve taslak önizlemeye katkısı sıfır.
102
138
  // Key'siz istek sunucuda legacy draft yazımıdır; kanonik mühür bilinçli `theme push` işidir.
103
- await pushTheme({ dir, url, token, draft: true, onRetry });
139
+ noteHandle(await pushTheme({ dir, url, token, draft: true, onRetry }));
104
140
  } catch (err) {
105
141
  error = err?.message ?? String(err); // senkron hatası reload'u engellemesin
106
142
  }
@@ -109,11 +145,6 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
109
145
  onReload?.({ file, synced: syncDraft && !error, clients: clients.size, error });
110
146
  });
111
147
 
112
- // Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
113
- if (syncDraft) {
114
- void pushTheme({ dir, url, token, draft: true, onRetry }).catch(() => {});
115
- }
116
-
117
148
  server.listen(port);
118
149
 
119
150
  return {
@@ -25,10 +25,14 @@ function canonicalHeaders(extra = {}) {
25
25
  async function errorText(res) {
26
26
  const text = await res.text();
27
27
  try {
28
- return JSON.parse(text).error ?? text;
28
+ const parsed = JSON.parse(text).error;
29
+ if (parsed) return parsed;
29
30
  } catch {
30
- return text;
31
+ /* JSON değil — ham metne düş */
31
32
  }
33
+ // İçeriksiz yanıt (ör. 410 tombstone) boş dize döndürüp `Error("")` üretiyordu; kullanıcı
34
+ // `unavailable ()` görüyordu. Gövde yoksa statü tek teşhis kaynağıdır — onu taşı.
35
+ return text || `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
32
36
  }
33
37
 
34
38
  /**
@@ -158,7 +162,19 @@ export async function fetchDevSession({ url, token, name = null }) {
158
162
  const res = await fetch(`${base}/api/dev/session${query}`, {
159
163
  headers: { authorization: `Bearer ${token}` },
160
164
  });
161
- if (!res.ok) throw new Error(await errorText(res));
165
+ if (!res.ok) {
166
+ const detail = await errorText(res);
167
+ // Yeni sunucular 410'da açıklayıcı bir gövde döndürür — onu olduğu gibi göster, o otoritedir.
168
+ // Gövdesiz 410 (eski sunucu) `errorText`'ten "HTTP 410 …" olarak gelir; o durumda teşhisi
169
+ // biz veririz, yoksa kullanıcı yalnız bir statü kodu görür.
170
+ if (res.status === 410 && /^HTTP 410\b/.test(detail)) {
171
+ throw new Error(
172
+ "CLI remote preview (the signed preview URL and the editor view) is retired on this server " +
173
+ "(HTTP 410). Draft sync and `theme publish` are unaffected — they use different endpoints.",
174
+ );
175
+ }
176
+ throw new Error(detail);
177
+ }
162
178
  return res.json();
163
179
  }
164
180
 
@@ -167,11 +183,12 @@ export async function fetchDevSession({ url, token, name = null }) {
167
183
  * writes to a draft theme instance instead of the live theme — preview & publish
168
184
  * it from the admin panel without affecting the live site.
169
185
  */
170
- export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, dryRun = false, idempotencyKey = null, onRetry = null }) {
186
+ export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, dryRun = false, idempotencyKey = null, onRetry = null, prune = false, confirmPrune = null }) {
171
187
  const base = url.replace(/\/+$/, "");
172
188
  const files = readLocalTemplates(dir);
173
189
  const headersFor = (extra = {}) => canonicalHeaders({ authorization: `Bearer ${token}`, ...extra });
174
190
  const remoteOnlyKept = [];
191
+ const remoteOnlyRemoved = [];
175
192
  if (idempotencyKey && !dryRun) {
176
193
  // 0.5.0 kanonik push üç aşamadır (denetim düzeltmeleri):
177
194
  // 1) PREFLIGHT dry-run POST (yalnız yerel küme): 422/426 sınıfı hatalar SUNUCUDA HİÇBİR ŞEY
@@ -179,8 +196,12 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
179
196
  // arkada instance + sayfa klonu bırakıyordu.
180
197
  // 2) MERGE PROBE GET: "push silmez" garantisi — uzakta olup yerelde olmayan, kapı-kabul-eden
181
198
  // dosyalar payload'a aynen eklenir; kapı-dışı satırları (README.md vb.) sunucunun
182
- // retained-rows kuralı korur. Taslak hedefte `?name=` taşınır: adsız probe taslağı
183
- // `CLI Draft — <tarih>` olarak yaratır ve POST'un --name'i reuse yolunda yok sayılırdı.
199
+ // retained-rows kuralı korur. `--prune` bu dosyaları EKLEMEZ (kanonik deploy klasörleri
200
+ // atomik değiştirir → silinirler) ve yazımdan ÖNCE `confirmPrune(liste)`'ye bildirir.
201
+ // PS-09: taslak hedefte taslak-bayraklı GET KULLANILMAZ — sunucuda taslak provizyonlar (yan
202
+ // etki; yarıda kalırsa boş taslak bırakırdı). Mevcut CLI taslağı `/api/dev/site`'tan
203
+ // (`source === "import"`, sunucunun ensureDraftInstance seçimiyle aynı: id sıralı ilk) bulunup
204
+ // `?instance=` ile yoklanır; yoksa yoklanacak uzak dosya da yoktur → probe atlanır.
184
205
  // 3) Gerçek POST. Dev-sync key göndermediği için bu aşamaların hiçbirini ödemez.
185
206
  const pre = await fetchWithRetry(
186
207
  `${base}/api/dev/theme`,
@@ -189,25 +210,37 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
189
210
  );
190
211
  if (!pre.ok) throw await httpError(pre);
191
212
 
192
- const query = instance
193
- ? `?instance=${encodeURIComponent(instance)}`
194
- : draft
195
- ? `?draft=1${name ? `&name=${encodeURIComponent(name)}` : ""}`
196
- : "";
197
- const probe = await fetch(`${base}/api/dev/theme${query}`, { headers: headersFor() });
198
- if (!probe.ok) throw await httpError(probe);
199
- const { files: remote = {} } = await probe.json();
213
+ let probeInstance = instance;
214
+ if (!instance && draft) {
215
+ const status = await fetchSiteStatus({ url, token });
216
+ probeInstance = (status?.drafts ?? []).find((d) => d.source === "import")?.id ?? null;
217
+ }
218
+ let remote = {};
219
+ if (probeInstance || !draft) {
220
+ const query = probeInstance ? `?instance=${encodeURIComponent(probeInstance)}` : "";
221
+ const probe = await fetch(`${base}/api/dev/theme${query}`, { headers: headersFor() });
222
+ if (!probe.ok) throw await httpError(probe);
223
+ remote = (await probe.json()).files ?? {};
224
+ }
200
225
  for (const [key, content] of Object.entries(remote)) {
201
226
  if (key in files) continue;
202
227
  const top = key.split("/")[0] ?? "";
203
228
  // Sunucu kapı-kabul kümesinin birebir aynası: THEME_DIRS altı ÇOK-SEGMENTLİ path'ler + tek
204
229
  // config istisnası (çıplak `layout` gibi slash'sız bir uzak anahtar sunucuda 422 olurdu).
205
230
  if ((key.includes("/") && THEME_DIRS.has(top)) || key === "config/settings_schema.json") {
206
- files[key] = typeof content === "string" ? content : "";
207
- remoteOnlyKept.push(key);
231
+ if (prune) {
232
+ remoteOnlyRemoved.push(key);
233
+ } else {
234
+ files[key] = typeof content === "string" ? content : "";
235
+ remoteOnlyKept.push(key);
236
+ }
208
237
  }
209
238
  }
210
239
  remoteOnlyKept.sort();
240
+ remoteOnlyRemoved.sort();
241
+ if (remoteOnlyRemoved.length && confirmPrune && !(await confirmPrune(remoteOnlyRemoved))) {
242
+ return { aborted: true, remoteOnlyRemoved };
243
+ }
211
244
  }
212
245
  const target = instance ? { files, instance } : name ? { files, draft, name } : { files, draft };
213
246
  // dryRun asks the server to validate (auth + snapshot + hostile-Liquid worker) WITHOUT writing.
@@ -221,7 +254,11 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
221
254
  );
222
255
  if (!res.ok) throw await httpError(res);
223
256
  const json = await res.json();
224
- return remoteOnlyKept.length ? { ...json, remoteOnlyKept } : json;
257
+ return {
258
+ ...json,
259
+ ...(remoteOnlyKept.length ? { remoteOnlyKept } : {}),
260
+ ...(remoteOnlyRemoved.length ? { remoteOnlyRemoved } : {}),
261
+ };
225
262
  }
226
263
 
227
264
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",