@blocofy/cli 0.4.0 → 0.6.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 +29 -0
- package/bin/blocofy.mjs +164 -26
- package/lib/args.mjs +10 -2
- package/lib/dev-server.mjs +42 -8
- package/lib/local-theme.mjs +8 -2
- package/lib/theme-sync.mjs +143 -13
- package/lib/validate.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,35 @@ blocofy --help
|
|
|
33
33
|
|
|
34
34
|
## Changelog
|
|
35
35
|
|
|
36
|
+
- **0.6.0** — `theme dev`'s local view finally shows what you are working on. It used to render your
|
|
37
|
+
DRAFT theme against the site's LIVE page content, so draft page documents were invisible; each render
|
|
38
|
+
now carries the draft theme instance and the platform resolves that instance's pages. Three failures
|
|
39
|
+
caused by a retired server endpoint (`/api/dev/session`, gone since the platform's preview-security
|
|
40
|
+
work) are fixed at the root: **draft sync** was switched off whenever that endpoint failed even though
|
|
41
|
+
sync never used it (it posts to `/api/dev/theme`); **`theme publish`** without `--instance` called it
|
|
42
|
+
with no error handling and died with an empty message — it now resolves the CLI draft from
|
|
43
|
+
`blocofy status` (`source: "import"`); and an empty response body produced the undiagnosable
|
|
44
|
+
`dev session unavailable ()` — errors now fall back to `HTTP <status>` and a 410 explains itself.
|
|
45
|
+
If the server ever stops returning a draft handle, the CLI now says so instead of silently rendering
|
|
46
|
+
live content. Remote (shared-link and editor) preview is NOT restored — that surface was retired on
|
|
47
|
+
the platform side; `theme dev` prints the local view only.
|
|
48
|
+
|
|
49
|
+
- **0.5.0** — M4 canonical deploy protocol. `theme push` now deploys through the platform's atomic
|
|
50
|
+
source pipeline: the CLI declares the protocol handshake and auto-generates a per-push idempotency
|
|
51
|
+
key (transport retries converge; `--idempotency-key <k>` overrides it for scripting). A `--live`
|
|
52
|
+
push is finally visible on the pinned production render again. "Push does not delete" still holds —
|
|
53
|
+
remote-only files are carried along, and server-retained rows survive. New flags: `--dry-run` /
|
|
54
|
+
`--validate` (server-side validation, nothing written — refused against a server that predates the
|
|
55
|
+
protocol, which would otherwise silently write), `--diff` (read-only compare vs the LIVE theme).
|
|
56
|
+
`--help` on ANY subcommand now prints help and never runs the command (0.4.0 executed a real push);
|
|
57
|
+
unknown flags exit write-free instead of silently swallowing the next argument. `layer/` joined the
|
|
58
|
+
synced theme directories. Server errors `cli_upgrade_required` (426) and `idempotency_conflict`
|
|
59
|
+
(409) get human messages. Token format check aligned to the server minimum (total length ≥24 incl. the
|
|
60
|
+
`bcf_` prefix; real tokens are 47 chars). Two honest costs of the canonical pipeline: re-pushing
|
|
61
|
+
identical bytes still records a NEW deployment (the revision itself is deduplicated server-side; on
|
|
62
|
+
a repo-linked live target it also produces a reconcile bot commit), and the no-delete guarantee is
|
|
63
|
+
bought by carrying remote-only files in the payload — a very large theme (local+remote > 256 files
|
|
64
|
+
or > 4 MiB) can now hit the server's payload limits where 0.4.0's upsert did not.
|
|
36
65
|
- **0.4.0** — `theme push` / `theme dev` accept `--name <name>` to name a new draft (applied
|
|
37
66
|
only when the draft is first created; reuse ignores it). New `theme rename <handle> <new name>`
|
|
38
67
|
renames a theme (the name is a label; works on any theme, including the live one).
|
package/bin/blocofy.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* `/api/dev/render` endpoint (local theme files + live data) with file-watch
|
|
9
9
|
* livereload. No monorepo required.
|
|
10
10
|
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { existsSync } from "node:fs";
|
|
12
13
|
import { createRequire } from "node:module";
|
|
13
14
|
import { resolve } from "node:path";
|
|
@@ -19,7 +20,7 @@ import { credentialsPath, loadCredentials, saveCredentials } from "../lib/creden
|
|
|
19
20
|
import { startDevServer } from "../lib/dev-server.mjs";
|
|
20
21
|
import { readLocalTemplates } from "../lib/local-theme.mjs";
|
|
21
22
|
import { githubNote, retryNotice, statusLine, syncScopeNote } from "../lib/messages.mjs";
|
|
22
|
-
import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme, renameInstance } from "../lib/theme-sync.mjs";
|
|
23
|
+
import { diffTheme, fetchCanonicalSupport, fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme, renameInstance } from "../lib/theme-sync.mjs";
|
|
23
24
|
import { isAffirmative, livePushDecision, resolvePushMode } from "../lib/confirm.mjs";
|
|
24
25
|
import { hyperlink, openUrl } from "../lib/term.mjs";
|
|
25
26
|
import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
|
|
@@ -27,6 +28,28 @@ import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
|
|
|
27
28
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
28
29
|
const args = process.argv.slice(2);
|
|
29
30
|
|
|
31
|
+
// 0.5.0 güvenlik sıkılaştırması: her komutun kabul ettiği bayraklar açık bir listedir. Bilinmeyen bir
|
|
32
|
+
// bayrak YAZIMSIZ çıkışla reddedilir (eskiden sonraki token'ı değer olarak yutup hedef dizini sessizce
|
|
33
|
+
// kaydırıyordu). `--confirm` ve `--dry` belgelenmemiş ama gerçek bayraklardır; `--dry` (theme dev'in
|
|
34
|
+
// sunucu-başlatmama kancası) `--dry-run`'dan (sunucu-tarafı doğrulama) FARKLIDIR, alias değildir.
|
|
35
|
+
const KNOWN = {
|
|
36
|
+
login: ["url", "token"],
|
|
37
|
+
themePull: ["draft", "instance"],
|
|
38
|
+
themePush: ["diff", "draft", "instance", "name", "live", "yes", "confirm", "dry-run", "validate", "idempotency-key"],
|
|
39
|
+
themeDev: ["port", "dry", "no-sync", "name"],
|
|
40
|
+
themePublish: ["instance"],
|
|
41
|
+
themeRename: ["name"],
|
|
42
|
+
content: [],
|
|
43
|
+
};
|
|
44
|
+
function parseArgsOrExit(rest, known) {
|
|
45
|
+
const parsed = parseArgs(rest, new Set([...known, "help", "version"]));
|
|
46
|
+
if (parsed.unknownFlag) {
|
|
47
|
+
console.error(`Unknown flag ${parsed.unknownFlag}. Nothing was written. See \`blocofy --help\`.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/** Human label for a resolved site: "Name (slug)" or just the slug. */
|
|
31
54
|
function siteLabel(site) {
|
|
32
55
|
if (!site) return "";
|
|
@@ -69,6 +92,10 @@ Usage
|
|
|
69
92
|
--instance <handle> push to a specific theme by its handle (safe targeted
|
|
70
93
|
write — no live-confirmation prompt)
|
|
71
94
|
--name <name> name the NEW draft (draft mode only; ignored on --live/--instance)
|
|
95
|
+
--dry-run validate on the server WITHOUT writing (auth + snapshot + Liquid check)
|
|
96
|
+
--validate alias for --dry-run (validate only, nothing written)
|
|
97
|
+
--diff show what a push WOULD change vs the target (read-only), then stop
|
|
98
|
+
--idempotency-key <k> attach an idempotency key so a retried push is not double-applied
|
|
72
99
|
|
|
73
100
|
blocofy theme rename <handle> <new name>
|
|
74
101
|
Rename a theme (the name is just a label). Works on any of your themes,
|
|
@@ -118,7 +145,7 @@ async function promptValid(rl, question, normalize, valid, hint, tries = 3) {
|
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
async function login(rest) {
|
|
121
|
-
const { flags } =
|
|
148
|
+
const { flags } = parseArgsOrExit(rest, KNOWN.login);
|
|
122
149
|
let url = typeof flags.url === "string" ? normalizeUrl(flags.url) : "";
|
|
123
150
|
let token = typeof flags.token === "string" ? flags.token.trim() : "";
|
|
124
151
|
|
|
@@ -186,7 +213,7 @@ function requireCreds() {
|
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
async function themePull(rest) {
|
|
189
|
-
const { flags, positionals } =
|
|
216
|
+
const { flags, positionals } = parseArgsOrExit(rest, KNOWN.themePull);
|
|
190
217
|
const dir = resolve(positionals[0] ?? process.cwd());
|
|
191
218
|
const creds = requireCreds();
|
|
192
219
|
const draft = Boolean(flags.draft);
|
|
@@ -197,7 +224,7 @@ async function themePull(rest) {
|
|
|
197
224
|
}
|
|
198
225
|
|
|
199
226
|
async function themePush(rest) {
|
|
200
|
-
const { flags, positionals } =
|
|
227
|
+
const { flags, positionals } = parseArgsOrExit(rest, KNOWN.themePush);
|
|
201
228
|
const dir = resolve(positionals[0] ?? process.cwd());
|
|
202
229
|
if (!existsSync(dir)) {
|
|
203
230
|
console.error(`Theme directory not found: ${dir}`);
|
|
@@ -206,6 +233,38 @@ async function themePush(rest) {
|
|
|
206
233
|
const creds = requireCreds();
|
|
207
234
|
const instanceFlag = typeof flags.instance === "string" ? flags.instance : null;
|
|
208
235
|
const name = typeof flags.name === "string" ? flags.name : null;
|
|
236
|
+
const dryRun = Boolean(flags["dry-run"] || flags.validate);
|
|
237
|
+
// 0.5.0: her push'a otomatik idempotency key — kanonik dal (protokol + key) ancak böyle seçilir; key
|
|
238
|
+
// OLMADAN header'lar tek başına legacy writer'a düşer ve --live push pinned render'a YANSIMAZ (M4
|
|
239
|
+
// read cutover'ının ana CLI şikâyeti). Push-OPERASYONU-başına üretilir: fetchWithRetry'nin 5xx/429
|
|
240
|
+
// denemeleri aynı key'le yakınsar, ardışık push'lar farklı key alır (sabit key, settings/hedef
|
|
241
|
+
// değişiminde kalıcı 409 idempotency_conflict üretirdi). Ham id gönderilir — sunucu `idem:` ile
|
|
242
|
+
// ad-alanlar, önek EKLENMEZ.
|
|
243
|
+
const idempotencyKey = typeof flags["idempotency-key"] === "string" ? flags["idempotency-key"] : `cli-${randomUUID()}`;
|
|
244
|
+
|
|
245
|
+
// `--diff`: read-only preview vs the LIVE theme (or --instance). No write; the draft target is not
|
|
246
|
+
// diffable (a draft GET would PROVISION the draft server-side — a read-only command must not mutate).
|
|
247
|
+
if (flags.diff) {
|
|
248
|
+
let diffSite = null;
|
|
249
|
+
try {
|
|
250
|
+
diffSite = (await fetchWhoami({ url: creds.url, token: creds.token })).site;
|
|
251
|
+
} catch {
|
|
252
|
+
/* best-effort — etiket kozmetik, diff yine koşar */
|
|
253
|
+
}
|
|
254
|
+
const diffTarget = diffSite ? ` of ${siteLabel(diffSite)}` : "";
|
|
255
|
+
const d = await diffTheme({ dir, url: creds.url, token: creds.token, instance: instanceFlag });
|
|
256
|
+
console.log(instanceFlag ? `Diff vs theme ${instanceFlag}${diffTarget}:` : `Diff vs the LIVE theme${diffTarget} (push default writes to a DRAFT):`);
|
|
257
|
+
const total = d.added.length + d.changed.length + d.removed.length;
|
|
258
|
+
if (total === 0) {
|
|
259
|
+
console.log("No differences — local theme matches the target.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
for (const k of d.added) console.log(` + ${k}`);
|
|
263
|
+
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)`);
|
|
265
|
+
console.log(`\n${d.added.length} added, ${d.changed.length} changed, ${d.removed.length} remote-only.`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
209
268
|
|
|
210
269
|
// Yeni varsayılan hedef: DRAFT (güvenli). `--live` eski anında-canlı davranışını
|
|
211
270
|
// açıkça geri getirir; `--instance` belirli bir temayı adresler. Sadece "live"
|
|
@@ -229,7 +288,7 @@ async function themePush(rest) {
|
|
|
229
288
|
if (mode === "instance") {
|
|
230
289
|
console.log(`→ Pushing to theme ${instance}${site ? ` of ${target}` : ""}`);
|
|
231
290
|
} else if (mode === "live") {
|
|
232
|
-
console.log(`→ Pushing to the LIVE theme of ${target}`);
|
|
291
|
+
console.log(dryRun ? `→ Validating against the LIVE theme of ${target} (dry run — nothing will be written)` : `→ Pushing to the LIVE theme of ${target}`);
|
|
233
292
|
} else {
|
|
234
293
|
console.log(`→ Pushing to a draft${site ? ` of ${target}` : ""}`);
|
|
235
294
|
}
|
|
@@ -237,8 +296,9 @@ async function themePush(rest) {
|
|
|
237
296
|
// Canlı push (`--live`) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
|
|
238
297
|
// kazara canlıya basmasın diye açık onay şart (#431 L2). Draft/instance modu
|
|
239
298
|
// güvenli → otomatik onay (prompt yok).
|
|
299
|
+
// 0.5.0: `--dry-run` hiçbir şey yazmaz → canlı onayı gereksiz (draft:true gibi davranır).
|
|
240
300
|
const decision = livePushDecision({
|
|
241
|
-
draft: mode !== "live",
|
|
301
|
+
draft: mode !== "live" || dryRun,
|
|
242
302
|
yes: Boolean(flags.yes),
|
|
243
303
|
confirm: Boolean(flags.confirm),
|
|
244
304
|
isTTY: Boolean(process.stdin.isTTY),
|
|
@@ -268,15 +328,65 @@ async function themePush(rest) {
|
|
|
268
328
|
console.error("Note: --name yalnız yeni taslak yaratırken (varsayılan push) geçerli, yok sayıldı.");
|
|
269
329
|
}
|
|
270
330
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
token: creds.token
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
331
|
+
// 0.5.0: eski (protokolsüz) bir sunucu `dryRun` alanını bilmez ve SESSİZCE GERÇEK YAZIM yapardı —
|
|
332
|
+
// dry-run yalnız sunucu kanonik protokolü beyan ediyorsa koşar (sorgusuz, mutasyonsuz GET ön kontrolü).
|
|
333
|
+
if (dryRun) {
|
|
334
|
+
const { supported } = await fetchCanonicalSupport({ url: creds.url, token: creds.token });
|
|
335
|
+
if (!supported) {
|
|
336
|
+
console.error("✗ --dry-run needs a server that speaks the canonical protocol; this one does not.");
|
|
337
|
+
console.error(" An old server would IGNORE dryRun and write for real. Nothing was sent.");
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
result = await pushTheme({
|
|
345
|
+
dir,
|
|
346
|
+
url: creds.url,
|
|
347
|
+
token: creds.token,
|
|
348
|
+
draft: mode === "draft",
|
|
349
|
+
instance: mode === "instance" ? instance : null,
|
|
350
|
+
name: mode === "draft" ? name : null,
|
|
351
|
+
dryRun,
|
|
352
|
+
idempotencyKey,
|
|
353
|
+
onRetry: (info) => console.error(retryNotice(info)),
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (error?.code === "cli_upgrade_required") {
|
|
357
|
+
const missing = Array.isArray(error?.body?.fence?.missing) ? error.body.fence.missing.join(", ") : "";
|
|
358
|
+
console.error("✗ Sunucu bu CLI sürümünü reddetti (cli_upgrade_required).");
|
|
359
|
+
console.error(` Güncelle: npm i -g @blocofy/cli@latest${missing ? `\n Sunucunun istediği eksik yetenekler: ${missing}` : ""}`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
if (error?.code === "idempotency_conflict") {
|
|
363
|
+
console.error("✗ Idempotency çakışması: aynı anahtar daha önce FARKLI içerikle kullanılmış (409).");
|
|
364
|
+
console.error(" `--idempotency-key` verdiysen yeni bir anahtarla dene; vermediysen tekrar `blocofy theme push` yeterli (her koşu taze anahtar üretir).");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --dry-run / --validate: the server validated without writing. Report and stop.
|
|
371
|
+
if (result.dryRun) {
|
|
372
|
+
const warnings = Array.isArray(result.warnings) ? result.warnings : [];
|
|
373
|
+
console.log(`✓ Validation passed (dry run — nothing written).${warnings.length ? ` ${warnings.length} warning(s).` : ""}`);
|
|
374
|
+
for (const w of warnings) console.log(` ⚠ ${w}`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 0.5.0: kanonik boru hattı yanıtı (CP üzerinden atomik deploy) — legacy created/updated alanları yok.
|
|
379
|
+
if (result.committed === true) {
|
|
380
|
+
if (Array.isArray(result.remoteOnlyKept) && result.remoteOnlyKept.length) {
|
|
381
|
+
console.log(` (kept ${result.remoteOnlyKept.length} remote-only file(s) — push does not delete)`);
|
|
382
|
+
}
|
|
383
|
+
console.log(`✓ Deployed atomically: deployment #${result.deploymentId}, revision #${result.sourceRevisionId}, pointer v${result.pointerVersion}.`);
|
|
384
|
+
if (mode === "draft") {
|
|
385
|
+
console.log("Preview & publish it in the admin panel: Theme -> Theme library -> \"Open in editor\".");
|
|
386
|
+
console.log("Publish it live with: blocofy theme publish");
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
280
390
|
|
|
281
391
|
// Sunucu canlı-yazımı bildirdiyse (yeni alan; eski sunucuda yok) belirgin uyar.
|
|
282
392
|
if (result.warning === "live_write" && result.message) {
|
|
@@ -300,7 +410,7 @@ async function themePush(rest) {
|
|
|
300
410
|
}
|
|
301
411
|
|
|
302
412
|
async function contentPush(scope, rest) {
|
|
303
|
-
const { positionals } =
|
|
413
|
+
const { positionals } = parseArgsOrExit(rest, KNOWN.content);
|
|
304
414
|
const dir = resolve(positionals[0] ?? process.cwd());
|
|
305
415
|
if (!existsSync(dir)) {
|
|
306
416
|
console.error(`Directory not found: ${dir}`);
|
|
@@ -322,7 +432,7 @@ async function contentPush(scope, rest) {
|
|
|
322
432
|
}
|
|
323
433
|
|
|
324
434
|
async function contentPull(scope, rest) {
|
|
325
|
-
const { positionals } =
|
|
435
|
+
const { positionals } = parseArgsOrExit(rest, KNOWN.content);
|
|
326
436
|
const dir = resolve(positionals[0] ?? process.cwd());
|
|
327
437
|
const creds = requireCreds();
|
|
328
438
|
const { count } = await pullContent({ dir, url: creds.url, token: creds.token, scope });
|
|
@@ -330,7 +440,7 @@ async function contentPull(scope, rest) {
|
|
|
330
440
|
}
|
|
331
441
|
|
|
332
442
|
async function themeDev(rest) {
|
|
333
|
-
const { flags, positionals } =
|
|
443
|
+
const { flags, positionals } = parseArgsOrExit(rest, KNOWN.themeDev);
|
|
334
444
|
const themeDir = resolve(positionals[0] ?? process.cwd());
|
|
335
445
|
|
|
336
446
|
if (!existsSync(themeDir)) {
|
|
@@ -350,8 +460,8 @@ async function themeDev(rest) {
|
|
|
350
460
|
session = await fetchDevSession({ url: creds.url, token: creds.token, name });
|
|
351
461
|
} catch (error) {
|
|
352
462
|
console.warn(
|
|
353
|
-
`Warning: dev session unavailable (${error?.message
|
|
354
|
-
`
|
|
463
|
+
`Warning: dev session unavailable (${error?.message || error}). ` +
|
|
464
|
+
`Live-domain and editor views are disabled; draft sync and local preview keep working.`,
|
|
355
465
|
);
|
|
356
466
|
}
|
|
357
467
|
}
|
|
@@ -413,8 +523,12 @@ async function themeDev(rest) {
|
|
|
413
523
|
url: creds.url,
|
|
414
524
|
token: creds.token,
|
|
415
525
|
port,
|
|
416
|
-
|
|
526
|
+
// Taslak senkronu dev session'a BAĞLI DEĞİL: `pushTheme({draft:true})` /api/dev/theme'e gider ve
|
|
527
|
+
// session'dan hiçbir veri kullanmaz. Bu satır `Boolean(session)` iken, session 410 alınca senkron
|
|
528
|
+
// da sessizce kapanıyordu — ölü bir uç, çalışan bir özelliği götürüyordu. Tek kapatma yolu --no-sync.
|
|
529
|
+
syncDraft: !flags["no-sync"],
|
|
417
530
|
onRetry: (info) => console.error(` ${retryNotice(info)}`),
|
|
531
|
+
onWarn: (msg) => console.warn(` ⚠ ${msg}`),
|
|
418
532
|
// Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
|
|
419
533
|
// = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
|
|
420
534
|
// hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
|
|
@@ -471,13 +585,30 @@ async function themeDev(rest) {
|
|
|
471
585
|
}
|
|
472
586
|
|
|
473
587
|
async function themePublish(rest) {
|
|
474
|
-
const { flags } =
|
|
588
|
+
const { flags } = parseArgsOrExit(rest, KNOWN.themePublish);
|
|
475
589
|
const creds = requireCreds();
|
|
476
590
|
let instance = typeof flags.instance === "string" ? flags.instance : null;
|
|
477
591
|
if (!instance) {
|
|
478
|
-
// Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla.
|
|
479
|
-
|
|
480
|
-
|
|
592
|
+
// Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla. Kaynak
|
|
593
|
+
// `GET /api/dev/site` — eski `fetchDevSession` yolu sunucuda 410'a döndü ve bu komutu
|
|
594
|
+
// try/catch'siz, boş mesajlı bir Error ile tamamen çalışmaz hâle getirmişti.
|
|
595
|
+
//
|
|
596
|
+
// `drafts` canlı OLMAYAN HER instance'ı içerir; sunucunun `ensureDraftInstance`'ı ise
|
|
597
|
+
// `source === "import"` olanı seçer (yayınlanan taslak import'tan çıkarılır). Aynı seçimi
|
|
598
|
+
// burada tekrarla — yoksa bir kez yayın yapmış her sitede iki taslak görünür ve komut takılır.
|
|
599
|
+
const status = await fetchSiteStatus({ url: creds.url, token: creds.token });
|
|
600
|
+
const drafts = status?.drafts ?? [];
|
|
601
|
+
const cliDrafts = drafts.filter((d) => d.source === "import");
|
|
602
|
+
if (cliDrafts.length === 1) {
|
|
603
|
+
instance = cliDrafts[0].id;
|
|
604
|
+
} else if (drafts.length === 0) {
|
|
605
|
+
console.error("No draft theme to publish. Create one first: blocofy theme push --draft");
|
|
606
|
+
process.exit(1);
|
|
607
|
+
} else {
|
|
608
|
+
console.error("Could not tell which draft to publish — pick one with --instance <handle>:");
|
|
609
|
+
for (const d of drafts) console.error(` ${d.id} ${d.name ?? "(unnamed)"}`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
481
612
|
}
|
|
482
613
|
const result = await publishInstance({ url: creds.url, token: creds.token, instanceId: instance });
|
|
483
614
|
console.log(
|
|
@@ -486,7 +617,7 @@ async function themePublish(rest) {
|
|
|
486
617
|
}
|
|
487
618
|
|
|
488
619
|
async function themeRename(rest) {
|
|
489
|
-
const { flags, positionals } =
|
|
620
|
+
const { flags, positionals } = parseArgsOrExit(rest, KNOWN.themeRename);
|
|
490
621
|
const handle = positionals[0];
|
|
491
622
|
const name = positionals.slice(1).join(" ") || (typeof flags.name === "string" ? flags.name : null);
|
|
492
623
|
if (!handle || !name) {
|
|
@@ -540,6 +671,13 @@ if (first === "--version" || first === "-v") {
|
|
|
540
671
|
console.log(VERSION);
|
|
541
672
|
} else if (!first || first === "--help" || first === "-h" || first === "help") {
|
|
542
673
|
printHelp();
|
|
674
|
+
} else if (args.includes("--help") || args.includes("-h")) {
|
|
675
|
+
// 0.5.0: `--help` HERHANGİ bir konumda YARDIMDIR — alt-komut handler'ı asla koşmaz.
|
|
676
|
+
// (0.4.0'da `blocofy theme push --help` GERÇEK bir push koşuyordu.)
|
|
677
|
+
printHelp();
|
|
678
|
+
} else if (args.includes("--version") || args.includes("-v")) {
|
|
679
|
+
// Aynı simetri: `theme push <dir> --version` da yalnız sürüm basar, asla yazmaz.
|
|
680
|
+
console.log(VERSION);
|
|
543
681
|
} else if (first === "login") {
|
|
544
682
|
login(rest).catch((error) => {
|
|
545
683
|
console.error(error?.message ?? error);
|
package/lib/args.mjs
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Sonraki token'ı değer olarak almayan flag'ler. */
|
|
8
|
-
const BOOLEAN_FLAGS = new Set(["draft", "live", "yes", "confirm", "no-sync", "dry", "help", "version"]);
|
|
8
|
+
const BOOLEAN_FLAGS = new Set(["draft", "live", "yes", "confirm", "no-sync", "dry", "dry-run", "validate", "diff", "help", "version"]);
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `known`: bu komutun kabul ettiği bayrak adları (Set). Verildiğinde bilinmeyen bir `--x`
|
|
12
|
+
* `unknownFlag` olarak döner ve DEĞER YUTMAZ — eskiden `--halp mydir` `mydir`'i değer sanıp
|
|
13
|
+
* hedef dizini sessizce cwd'ye kaydırıyordu (0.5.0: bilinmeyen bayrak = yazımsız çıkış).
|
|
14
|
+
*/
|
|
15
|
+
export function parseArgs(rest, known = null) {
|
|
11
16
|
const flags = {};
|
|
12
17
|
const positionals = [];
|
|
13
18
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -17,6 +22,9 @@ export function parseArgs(rest) {
|
|
|
17
22
|
continue;
|
|
18
23
|
}
|
|
19
24
|
const key = arg.slice(2);
|
|
25
|
+
if (known && !known.has(key)) {
|
|
26
|
+
return { flags, positionals, unknownFlag: `--${key}` };
|
|
27
|
+
}
|
|
20
28
|
if (BOOLEAN_FLAGS.has(key)) {
|
|
21
29
|
flags[key] = true;
|
|
22
30
|
continue;
|
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) {
|
|
@@ -97,7 +133,10 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
97
133
|
let error = null;
|
|
98
134
|
if (syncDraft) {
|
|
99
135
|
try {
|
|
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
|
|
137
|
+
// başına revizyon+deployment üretirdi (retention yok) ve taslak önizlemeye katkısı sıfır.
|
|
138
|
+
// Key'siz istek sunucuda legacy draft yazımıdır; kanonik mühür bilinçli `theme push` işidir.
|
|
139
|
+
noteHandle(await pushTheme({ dir, url, token, draft: true, onRetry }));
|
|
101
140
|
} catch (err) {
|
|
102
141
|
error = err?.message ?? String(err); // senkron hatası reload'u engellemesin
|
|
103
142
|
}
|
|
@@ -106,11 +145,6 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
106
145
|
onReload?.({ file, synced: syncDraft && !error, clients: clients.size, error });
|
|
107
146
|
});
|
|
108
147
|
|
|
109
|
-
// Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
|
|
110
|
-
if (syncDraft) {
|
|
111
|
-
void pushTheme({ dir, url, token, draft: true, onRetry }).catch(() => {});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
148
|
server.listen(port);
|
|
115
149
|
|
|
116
150
|
return {
|
package/lib/local-theme.mjs
CHANGED
|
@@ -7,7 +7,9 @@ import { extname, join, relative, sep } from "node:path";
|
|
|
7
7
|
* THEME_DIRS, keyed by relative path with the `.liquid` extension stripped.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Mirror of the platform's theme-paths.ts THEME_DIRS (incl. `layer`, added M5 B1 — 0.5.0 closes the
|
|
11
|
+
// drift where a pulled layer/ file was silently dropped from the next push).
|
|
12
|
+
export const THEME_DIRS = new Set(["layout", "section", "block", "layer", "partial", "asset", "template"]);
|
|
11
13
|
const LIQUID_KINDS = new Set(["layout", "section", "block", "partial", "template"]);
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -16,7 +18,11 @@ const LIQUID_KINDS = new Set(["layout", "section", "block", "partial", "template
|
|
|
16
18
|
*/
|
|
17
19
|
export function localPathFor(key) {
|
|
18
20
|
const top = key.split("/")[0] ?? "";
|
|
19
|
-
|
|
21
|
+
// Platform repoPathFor aynası (0.5.0): `.liquid` yalnız UZANTISIZ basename'lere — `template/index.json`
|
|
22
|
+
// diske AYNEN yazılır; eski kural onu `.json.liquid` yapıp git-checkout'lu bir çalışma dizininde
|
|
23
|
+
// repo'daki gerçek dosyanın yanına churn-ikizi düşürüyordu.
|
|
24
|
+
const base = key.split("/").pop() ?? "";
|
|
25
|
+
return LIQUID_KINDS.has(top) && !base.includes(".") ? `${key}.liquid` : key;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/** Turn a theme directory into a `{ path: content }` map (sent to dev-render). */
|
package/lib/theme-sync.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { localPathFor, readLocalTemplates } from "./local-theme.mjs";
|
|
4
|
+
import { THEME_DIRS, localPathFor, readLocalTemplates } from "./local-theme.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* `blocofy theme pull/push`. Talks to the platform's `/api/dev/theme` endpoint
|
|
@@ -9,13 +9,66 @@ import { localPathFor, readLocalTemplates } from "./local-theme.mjs";
|
|
|
9
9
|
* readLocalTemplates → POST (create/update; no delete).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* M4 canonical source-write handshake. We declare the protocol version + the full canonical-write
|
|
14
|
+
* capability set; the server is authoritative and fences an under-declaring client (a NEW CLI against an
|
|
15
|
+
* OLD server just sees the headers ignored — forward compatible). Capabilities must match the server's
|
|
16
|
+
* CANONICAL_WRITE_CAPABILITIES exactly, in order.
|
|
17
|
+
*/
|
|
18
|
+
const CANONICAL_PROTOCOL = "1";
|
|
19
|
+
const CANONICAL_CAPABILITIES = "validate,dry-run,diff,idempotency-key,target-instance";
|
|
20
|
+
|
|
21
|
+
function canonicalHeaders(extra = {}) {
|
|
22
|
+
return { "x-blocofy-protocol": CANONICAL_PROTOCOL, "x-blocofy-capabilities": CANONICAL_CAPABILITIES, ...extra };
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
async function errorText(res) {
|
|
13
26
|
const text = await res.text();
|
|
14
27
|
try {
|
|
15
|
-
|
|
28
|
+
const parsed = JSON.parse(text).error;
|
|
29
|
+
if (parsed) return parsed;
|
|
30
|
+
} catch {
|
|
31
|
+
/* JSON değil — ham metne düş */
|
|
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}` : ""}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Yapılandırılmış HTTP hatası (0.5.0): mesajın yanında `code` (sunucunun `error` alanı), `status` ve tam
|
|
40
|
+
* `body` taşınır — 426 `cli_upgrade_required` (fence.missing/requiredVersion) ve 409 `idempotency_conflict`
|
|
41
|
+
* için insan-dili mesajı ÇAĞIRAN kurar; buradaki throw yalnız veriyi kaybetmeden taşır.
|
|
42
|
+
*/
|
|
43
|
+
async function httpError(res) {
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
let body = null;
|
|
46
|
+
try {
|
|
47
|
+
body = JSON.parse(text);
|
|
16
48
|
} catch {
|
|
17
|
-
|
|
49
|
+
/* düz metin gövde */
|
|
18
50
|
}
|
|
51
|
+
const err = new Error(typeof body?.error === "string" ? body.error : text || `HTTP ${res.status}`);
|
|
52
|
+
err.status = res.status;
|
|
53
|
+
err.code = typeof body?.error === "string" ? body.error : null;
|
|
54
|
+
err.body = body;
|
|
55
|
+
return err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sunucu kanonik protokolü konuşuyor mu? (0.5.0 `--dry-run` ön kontrolü.) SORGUSUZ canlı GET — mutasyonsuz
|
|
60
|
+
* (`?draft=1` GET'i sunucuda taslak provizyonlar, o yüzden burada KULLANILMAZ). Eski bir sunucu `protocol`
|
|
61
|
+
* alanını hiç dönmez → false → `--dry-run` reddedilir; aksi hâlde eski sunucu `dryRun`'ı yok sayıp GERÇEK
|
|
62
|
+
* yazım yapardı (0.5.0 öncesi sessiz-yazım regresyonu).
|
|
63
|
+
*/
|
|
64
|
+
export async function fetchCanonicalSupport({ url, token }) {
|
|
65
|
+
const base = url.replace(/\/+$/, "");
|
|
66
|
+
const res = await fetch(`${base}/api/dev/theme`, {
|
|
67
|
+
headers: canonicalHeaders({ authorization: `Bearer ${token}` }),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) throw await httpError(res);
|
|
70
|
+
const body = await res.json();
|
|
71
|
+
return { supported: body?.protocol === 1 };
|
|
19
72
|
}
|
|
20
73
|
|
|
21
74
|
/** Backoff delays (ms) between attempts — index 0 = after the first failure. */
|
|
@@ -70,7 +123,7 @@ export async function pullTheme({ dir, url, token, draft = false, instance = nul
|
|
|
70
123
|
const base = url.replace(/\/+$/, "");
|
|
71
124
|
const query = instance ? `?instance=${encodeURIComponent(instance)}` : draft ? "?draft=1" : "";
|
|
72
125
|
const res = await fetch(`${base}/api/dev/theme${query}`, {
|
|
73
|
-
headers: { authorization: `Bearer ${token}` },
|
|
126
|
+
headers: canonicalHeaders({ authorization: `Bearer ${token}` }),
|
|
74
127
|
});
|
|
75
128
|
if (!res.ok) throw new Error(await errorText(res));
|
|
76
129
|
const { files } = await res.json();
|
|
@@ -109,7 +162,19 @@ export async function fetchDevSession({ url, token, name = null }) {
|
|
|
109
162
|
const res = await fetch(`${base}/api/dev/session${query}`, {
|
|
110
163
|
headers: { authorization: `Bearer ${token}` },
|
|
111
164
|
});
|
|
112
|
-
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
|
+
}
|
|
113
178
|
return res.json();
|
|
114
179
|
}
|
|
115
180
|
|
|
@@ -118,21 +183,86 @@ export async function fetchDevSession({ url, token, name = null }) {
|
|
|
118
183
|
* writes to a draft theme instance instead of the live theme — preview & publish
|
|
119
184
|
* it from the admin panel without affecting the live site.
|
|
120
185
|
*/
|
|
121
|
-
export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, onRetry = null }) {
|
|
186
|
+
export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, dryRun = false, idempotencyKey = null, onRetry = null }) {
|
|
122
187
|
const base = url.replace(/\/+$/, "");
|
|
123
188
|
const files = readLocalTemplates(dir);
|
|
124
|
-
const
|
|
189
|
+
const headersFor = (extra = {}) => canonicalHeaders({ authorization: `Bearer ${token}`, ...extra });
|
|
190
|
+
const remoteOnlyKept = [];
|
|
191
|
+
if (idempotencyKey && !dryRun) {
|
|
192
|
+
// 0.5.0 kanonik push üç aşamadır (denetim düzeltmeleri):
|
|
193
|
+
// 1) PREFLIGHT dry-run POST (yalnız yerel küme): 422/426 sınıfı hatalar SUNUCUDA HİÇBİR ŞEY
|
|
194
|
+
// provizyonlanmadan yakalanır — probe GET'i taslak yaratabildiği için başarısız bir push
|
|
195
|
+
// arkada instance + sayfa klonu bırakıyordu.
|
|
196
|
+
// 2) MERGE PROBE GET: "push silmez" garantisi — uzakta olup yerelde olmayan, kapı-kabul-eden
|
|
197
|
+
// 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ı.
|
|
200
|
+
// 3) Gerçek POST. Dev-sync key göndermediği için bu aşamaların hiçbirini ödemez.
|
|
201
|
+
const pre = await fetchWithRetry(
|
|
202
|
+
`${base}/api/dev/theme`,
|
|
203
|
+
{ method: "POST", headers: headersFor({ "content-type": "application/json", "x-idempotency-key": idempotencyKey }), body: JSON.stringify({ ...(instance ? { files, instance } : { files, draft, ...(name ? { name } : {}) }), dryRun: true }) },
|
|
204
|
+
{ onRetry },
|
|
205
|
+
);
|
|
206
|
+
if (!pre.ok) throw await httpError(pre);
|
|
207
|
+
|
|
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();
|
|
216
|
+
for (const [key, content] of Object.entries(remote)) {
|
|
217
|
+
if (key in files) continue;
|
|
218
|
+
const top = key.split("/")[0] ?? "";
|
|
219
|
+
// Sunucu kapı-kabul kümesinin birebir aynası: THEME_DIRS altı ÇOK-SEGMENTLİ path'ler + tek
|
|
220
|
+
// config istisnası (çıplak `layout` gibi slash'sız bir uzak anahtar sunucuda 422 olurdu).
|
|
221
|
+
if ((key.includes("/") && THEME_DIRS.has(top)) || key === "config/settings_schema.json") {
|
|
222
|
+
files[key] = typeof content === "string" ? content : "";
|
|
223
|
+
remoteOnlyKept.push(key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
remoteOnlyKept.sort();
|
|
227
|
+
}
|
|
228
|
+
const target = instance ? { files, instance } : name ? { files, draft, name } : { files, draft };
|
|
229
|
+
// dryRun asks the server to validate (auth + snapshot + hostile-Liquid worker) WITHOUT writing.
|
|
230
|
+
const payload = dryRun ? { ...target, dryRun: true } : target;
|
|
231
|
+
const headers = canonicalHeaders({ "content-type": "application/json", authorization: `Bearer ${token}` });
|
|
232
|
+
if (idempotencyKey) headers["x-idempotency-key"] = idempotencyKey;
|
|
125
233
|
const res = await fetchWithRetry(
|
|
126
234
|
`${base}/api/dev/theme`,
|
|
127
|
-
{
|
|
128
|
-
method: "POST",
|
|
129
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
130
|
-
body: JSON.stringify(payload),
|
|
131
|
-
},
|
|
235
|
+
{ method: "POST", headers, body: JSON.stringify(payload) },
|
|
132
236
|
{ onRetry },
|
|
133
237
|
);
|
|
238
|
+
if (!res.ok) throw await httpError(res);
|
|
239
|
+
const json = await res.json();
|
|
240
|
+
return remoteOnlyKept.length ? { ...json, remoteOnlyKept } : json;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* `blocofy theme push --diff` — compare the LOCAL theme with the LIVE theme (or an explicit `--instance`).
|
|
245
|
+
* Read-only: the draft target is deliberately NOT diffable — `?draft=1` GET'i sunucuda taslak PROVİZYONLAR
|
|
246
|
+
* (sayfa klonları dahil), read-only bir komut mutasyon tetikleyemez (0.5.0 denetim düzeltmesi). Çağıran
|
|
247
|
+
* çıktıyı "canlıya göre fark" diye etiketler. Returns `{ added, changed, removed }` (stripped keys).
|
|
248
|
+
*/
|
|
249
|
+
export async function diffTheme({ dir, url, token, instance = null }) {
|
|
250
|
+
const base = url.replace(/\/+$/, "");
|
|
251
|
+
const query = instance ? `?instance=${encodeURIComponent(instance)}` : "";
|
|
252
|
+
const res = await fetch(`${base}/api/dev/theme${query}`, {
|
|
253
|
+
headers: canonicalHeaders({ authorization: `Bearer ${token}` }),
|
|
254
|
+
});
|
|
134
255
|
if (!res.ok) throw new Error(await errorText(res));
|
|
135
|
-
|
|
256
|
+
const { files: remote = {} } = await res.json();
|
|
257
|
+
const local = readLocalTemplates(dir);
|
|
258
|
+
const added = [];
|
|
259
|
+
const changed = [];
|
|
260
|
+
for (const [key, content] of Object.entries(local)) {
|
|
261
|
+
if (!(key in remote)) added.push(key);
|
|
262
|
+
else if (remote[key] !== content) changed.push(key);
|
|
263
|
+
}
|
|
264
|
+
const removed = Object.keys(remote).filter((k) => !(k in local));
|
|
265
|
+
return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
|
|
136
266
|
}
|
|
137
267
|
|
|
138
268
|
/**
|
package/lib/validate.mjs
CHANGED