@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 +24 -0
- package/bin/blocofy.mjs +73 -10
- package/lib/dev-server.mjs +39 -8
- package/lib/theme-sync.mjs +54 -17
- 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,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
|
-
|
|
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
|
|
464
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
590
|
-
|
|
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(
|
package/lib/dev-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/lib/theme-sync.mjs
CHANGED
|
@@ -25,10 +25,14 @@ function canonicalHeaders(extra = {}) {
|
|
|
25
25
|
async function errorText(res) {
|
|
26
26
|
const text = await res.text();
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
const parsed = JSON.parse(text).error;
|
|
29
|
+
if (parsed) return parsed;
|
|
29
30
|
} catch {
|
|
30
|
-
|
|
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)
|
|
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.
|
|
183
|
-
//
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!
|
|
199
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
|
257
|
+
return {
|
|
258
|
+
...json,
|
|
259
|
+
...(remoteOnlyKept.length ? { remoteOnlyKept } : {}),
|
|
260
|
+
...(remoteOnlyRemoved.length ? { remoteOnlyRemoved } : {}),
|
|
261
|
+
};
|
|
225
262
|
}
|
|
226
263
|
|
|
227
264
|
/**
|