@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 +11 -0
- package/bin/blocofy.mjs +46 -4
- package/lib/theme-sync.mjs +35 -14
- package/package.json +1 -1
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
|
-
|
|
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\".");
|
package/lib/theme-sync.mjs
CHANGED
|
@@ -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.
|
|
199
|
-
//
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!
|
|
215
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
|
257
|
+
return {
|
|
258
|
+
...json,
|
|
259
|
+
...(remoteOnlyKept.length ? { remoteOnlyKept } : {}),
|
|
260
|
+
...(remoteOnlyRemoved.length ? { remoteOnlyRemoved } : {}),
|
|
261
|
+
};
|
|
241
262
|
}
|
|
242
263
|
|
|
243
264
|
/**
|