@blocofy/cli 0.6.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,15 @@ 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
+
36
47
  - **0.6.0** — `theme dev`'s local view finally shows what you are working on. It used to render your
37
48
  DRAFT theme against the site's LIVE page content, so draft page documents were invisible; each render
38
49
  now carries the draft theme instance and the platform resolves that instance's pages. Three failures
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\".");
@@ -183,11 +183,12 @@ export async function fetchDevSession({ url, token, name = null }) {
183
183
  * writes to a draft theme instance instead of the live theme — preview & publish
184
184
  * it from the admin panel without affecting the live site.
185
185
  */
186
- 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 }) {
187
187
  const base = url.replace(/\/+$/, "");
188
188
  const files = readLocalTemplates(dir);
189
189
  const headersFor = (extra = {}) => canonicalHeaders({ authorization: `Bearer ${token}`, ...extra });
190
190
  const remoteOnlyKept = [];
191
+ const remoteOnlyRemoved = [];
191
192
  if (idempotencyKey && !dryRun) {
192
193
  // 0.5.0 kanonik push üç aşamadır (denetim düzeltmeleri):
193
194
  // 1) PREFLIGHT dry-run POST (yalnız yerel küme): 422/426 sınıfı hatalar SUNUCUDA HİÇBİR ŞEY
@@ -195,8 +196,12 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
195
196
  // arkada instance + sayfa klonu bırakıyordu.
196
197
  // 2) MERGE PROBE GET: "push silmez" garantisi — uzakta olup yerelde olmayan, kapı-kabul-eden
197
198
  // dosyalar payload'a aynen eklenir; kapı-dışı satırları (README.md vb.) sunucunun
198
- // retained-rows kuralı korur. Taslak hedefte `?name=` taşınır: adsız probe taslağı
199
- // `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.
200
205
  // 3) Gerçek POST. Dev-sync key göndermediği için bu aşamaların hiçbirini ödemez.
201
206
  const pre = await fetchWithRetry(
202
207
  `${base}/api/dev/theme`,
@@ -205,25 +210,37 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
205
210
  );
206
211
  if (!pre.ok) throw await httpError(pre);
207
212
 
208
- const query = instance
209
- ? `?instance=${encodeURIComponent(instance)}`
210
- : draft
211
- ? `?draft=1${name ? `&name=${encodeURIComponent(name)}` : ""}`
212
- : "";
213
- const probe = await fetch(`${base}/api/dev/theme${query}`, { headers: headersFor() });
214
- if (!probe.ok) throw await httpError(probe);
215
- 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
+ }
216
225
  for (const [key, content] of Object.entries(remote)) {
217
226
  if (key in files) continue;
218
227
  const top = key.split("/")[0] ?? "";
219
228
  // Sunucu kapı-kabul kümesinin birebir aynası: THEME_DIRS altı ÇOK-SEGMENTLİ path'ler + tek
220
229
  // config istisnası (çıplak `layout` gibi slash'sız bir uzak anahtar sunucuda 422 olurdu).
221
230
  if ((key.includes("/") && THEME_DIRS.has(top)) || key === "config/settings_schema.json") {
222
- files[key] = typeof content === "string" ? content : "";
223
- remoteOnlyKept.push(key);
231
+ if (prune) {
232
+ remoteOnlyRemoved.push(key);
233
+ } else {
234
+ files[key] = typeof content === "string" ? content : "";
235
+ remoteOnlyKept.push(key);
236
+ }
224
237
  }
225
238
  }
226
239
  remoteOnlyKept.sort();
240
+ remoteOnlyRemoved.sort();
241
+ if (remoteOnlyRemoved.length && confirmPrune && !(await confirmPrune(remoteOnlyRemoved))) {
242
+ return { aborted: true, remoteOnlyRemoved };
243
+ }
227
244
  }
228
245
  const target = instance ? { files, instance } : name ? { files, draft, name } : { files, draft };
229
246
  // dryRun asks the server to validate (auth + snapshot + hostile-Liquid worker) WITHOUT writing.
@@ -237,7 +254,11 @@ export async function pushTheme({ dir, url, token, draft = false, instance = nul
237
254
  );
238
255
  if (!res.ok) throw await httpError(res);
239
256
  const json = await res.json();
240
- return remoteOnlyKept.length ? { ...json, remoteOnlyKept } : json;
257
+ return {
258
+ ...json,
259
+ ...(remoteOnlyKept.length ? { remoteOnlyKept } : {}),
260
+ ...(remoteOnlyRemoved.length ? { remoteOnlyRemoved } : {}),
261
+ };
241
262
  }
242
263
 
243
264
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.6.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",