@blocofy/cli 0.2.3 → 0.4.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 +19 -2
- package/bin/blocofy.mjs +104 -25
- package/lib/args.mjs +1 -1
- package/lib/confirm.mjs +16 -0
- package/lib/dev-server.mjs +3 -3
- package/lib/local-theme.mjs +4 -1
- package/lib/messages.mjs +12 -2
- package/lib/theme-sync.mjs +73 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,13 +17,29 @@ blocofy login [--url <url>] [--token <bcf_…>]
|
|
|
17
17
|
# Save your platform URL + dev token (~/.blocofy/credentials.json, 0600).
|
|
18
18
|
blocofy theme dev [dir] # Local dev server. Prints 3 views — local preview, a live-domain
|
|
19
19
|
# preview link, and the theme editor — all auto-reloading on save.
|
|
20
|
-
# --port <n> (default 3030), --no-sync (local preview only)
|
|
20
|
+
# --port <n> (default 3030), --no-sync (local preview only),
|
|
21
|
+
# --name <name> (name the draft when first created)
|
|
21
22
|
blocofy theme pull [dir] # Download the live theme to disk.
|
|
22
|
-
blocofy theme push [dir] # Write the local theme to
|
|
23
|
+
blocofy theme push [dir] # Write the local theme to a DRAFT by default (create/update; no
|
|
24
|
+
# delete) — preview & publish from the admin panel. --live writes
|
|
25
|
+
# to the live site immediately (asks to confirm; add --yes for CI).
|
|
26
|
+
# --name <name> names the new draft (draft mode only).
|
|
27
|
+
blocofy theme rename <handle> <new name>
|
|
28
|
+
# Rename a theme (label only). Works on any theme, live included.
|
|
29
|
+
# Handle from the panel theme card or `blocofy status`.
|
|
23
30
|
blocofy --version
|
|
24
31
|
blocofy --help
|
|
25
32
|
```
|
|
26
33
|
|
|
34
|
+
## Changelog
|
|
35
|
+
|
|
36
|
+
- **0.4.0** — `theme push` / `theme dev` accept `--name <name>` to name a new draft (applied
|
|
37
|
+
only when the draft is first created; reuse ignores it). New `theme rename <handle> <new name>`
|
|
38
|
+
renames a theme (the name is a label; works on any theme, including the live one).
|
|
39
|
+
- **0.3.0** — `theme push` now writes to a **draft** by default; use `--live` for the old
|
|
40
|
+
immediate-live behavior (with a confirmation prompt; `--yes` to skip it in CI). `blocofy
|
|
41
|
+
status` now names the exact pages at 404 risk when a theme's pages are split off the live theme.
|
|
42
|
+
|
|
27
43
|
## How it works
|
|
28
44
|
|
|
29
45
|
`theme dev` starts a local HTTP server. For each page request it reads your local theme
|
|
@@ -53,6 +69,7 @@ A theme is a directory of files grouped by top-level folder:
|
|
|
53
69
|
| `asset/` | CSS and static files (`theme.css`) |
|
|
54
70
|
| `pages/<slug>.json` | Page content (`/` → `pages/index.json`) |
|
|
55
71
|
| `config/settings.json` | Theme settings + color schemes |
|
|
72
|
+
| `config/settings_schema.json` | Theme settings panel schema (synced with `pull`/`push`) |
|
|
56
73
|
|
|
57
74
|
Liquid templates use the `.liquid` extension; files under `asset/` are served as-is.
|
|
58
75
|
|
package/bin/blocofy.mjs
CHANGED
|
@@ -18,9 +18,9 @@ import { pullContent, pushContent } from "../lib/content-sync.mjs";
|
|
|
18
18
|
import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
|
|
19
19
|
import { startDevServer } from "../lib/dev-server.mjs";
|
|
20
20
|
import { readLocalTemplates } from "../lib/local-theme.mjs";
|
|
21
|
-
import { githubNote, statusLine, syncScopeNote } from "../lib/messages.mjs";
|
|
22
|
-
import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
23
|
-
import { isAffirmative, livePushDecision } from "../lib/confirm.mjs";
|
|
21
|
+
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 { isAffirmative, livePushDecision, resolvePushMode } from "../lib/confirm.mjs";
|
|
24
24
|
import { hyperlink, openUrl } from "../lib/term.mjs";
|
|
25
25
|
import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
|
|
26
26
|
|
|
@@ -43,13 +43,14 @@ Usage
|
|
|
43
43
|
Save your platform URL + dev token to ~/.blocofy/credentials.json.
|
|
44
44
|
Get a token from the admin panel → Settings → Theme CLI tokens.
|
|
45
45
|
|
|
46
|
-
blocofy theme dev [dir] [--port <n>] [--no-sync]
|
|
46
|
+
blocofy theme dev [dir] [--port <n>] [--no-sync] [--name <name>]
|
|
47
47
|
Start a dev server and print 3 auto-reloading views — Local, live-domain
|
|
48
48
|
Preview, and the theme Editor. Press l / p / e to open each, q to quit.
|
|
49
49
|
Edit a file and save → every open view reloads. Saves sync to a DRAFT theme
|
|
50
50
|
only (never the live site). (dir defaults to cwd)
|
|
51
51
|
--port <n> local port (default 3030)
|
|
52
52
|
--no-sync local preview only (skip draft sync + remote views)
|
|
53
|
+
--name <name> name the draft when it is first created (ignored if it already exists)
|
|
53
54
|
|
|
54
55
|
blocofy theme pull [dir] [--draft] [--instance <handle>]
|
|
55
56
|
Download the live theme to disk. (dir defaults to cwd)
|
|
@@ -57,14 +58,21 @@ Usage
|
|
|
57
58
|
--instance <handle> pull a specific theme by its handle (from the admin
|
|
58
59
|
panel theme card, or \`blocofy status\`)
|
|
59
60
|
|
|
60
|
-
blocofy theme push [dir] [--
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
blocofy theme push [dir] [--live] [--yes] [--instance <handle>]
|
|
62
|
+
By DEFAULT writes to a DRAFT theme (create/update; no delete) — preview & publish
|
|
63
|
+
it from the admin panel, never touching the live site. Publish it with
|
|
64
|
+
'blocofy theme publish'.
|
|
65
|
+
--live write to the LIVE site IMMEDIATELY (no preview). Asks for
|
|
66
|
+
confirmation first; non-interactive shells must add --yes.
|
|
67
|
+
--draft explicit draft (same as the default; safe)
|
|
68
|
+
--yes confirm a --live push without prompting (for CI / agents)
|
|
66
69
|
--instance <handle> push to a specific theme by its handle (safe targeted
|
|
67
70
|
write — no live-confirmation prompt)
|
|
71
|
+
--name <name> name the NEW draft (draft mode only; ignored on --live/--instance)
|
|
72
|
+
|
|
73
|
+
blocofy theme rename <handle> <new name>
|
|
74
|
+
Rename a theme (the name is just a label). Works on any of your themes,
|
|
75
|
+
including the live one. Handle comes from the panel theme card or 'blocofy status'.
|
|
68
76
|
|
|
69
77
|
blocofy theme publish [--instance <handle>]
|
|
70
78
|
Publish a draft theme to the LIVE site. With no flag, publishes the draft that
|
|
@@ -90,7 +98,7 @@ Usage
|
|
|
90
98
|
Examples
|
|
91
99
|
blocofy login --url https://store.myblocofy.com --token bcf_xxxxxxxx
|
|
92
100
|
blocofy theme pull && blocofy theme dev
|
|
93
|
-
blocofy theme push
|
|
101
|
+
blocofy theme push && blocofy theme publish
|
|
94
102
|
blocofy status
|
|
95
103
|
|
|
96
104
|
Auth: ~/.blocofy/credentials.json (from \`login\`), or BLOCOFY_URL + BLOCOFY_TOKEN env vars.
|
|
@@ -196,8 +204,17 @@ async function themePush(rest) {
|
|
|
196
204
|
process.exit(1);
|
|
197
205
|
}
|
|
198
206
|
const creds = requireCreds();
|
|
199
|
-
const
|
|
200
|
-
const
|
|
207
|
+
const instanceFlag = typeof flags.instance === "string" ? flags.instance : null;
|
|
208
|
+
const name = typeof flags.name === "string" ? flags.name : null;
|
|
209
|
+
|
|
210
|
+
// Yeni varsayılan hedef: DRAFT (güvenli). `--live` eski anında-canlı davranışını
|
|
211
|
+
// açıkça geri getirir; `--instance` belirli bir temayı adresler. Sadece "live"
|
|
212
|
+
// modu canlıya yazar ve onay gerektirir.
|
|
213
|
+
const { mode, instance } = resolvePushMode({
|
|
214
|
+
live: Boolean(flags.live),
|
|
215
|
+
draft: Boolean(flags.draft),
|
|
216
|
+
instance: instanceFlag,
|
|
217
|
+
});
|
|
201
218
|
|
|
202
219
|
// Hedef tenant'ı çöz ve GÖSTER — site sunucuda TOKEN'dan çözülür (URL kozmetik),
|
|
203
220
|
// yanlış-tenant'a yazımı görünür kılar ("klarosa sandım, ksc'ye yazdım"). whoami
|
|
@@ -209,21 +226,26 @@ async function themePush(rest) {
|
|
|
209
226
|
/* best-effort */
|
|
210
227
|
}
|
|
211
228
|
const target = site ? siteLabel(site) : creds.url;
|
|
212
|
-
if (
|
|
213
|
-
console.log(`→
|
|
229
|
+
if (mode === "instance") {
|
|
230
|
+
console.log(`→ Pushing to theme ${instance}${site ? ` of ${target}` : ""}`);
|
|
231
|
+
} else if (mode === "live") {
|
|
232
|
+
console.log(`→ Pushing to the LIVE theme of ${target}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.log(`→ Pushing to a draft${site ? ` of ${target}` : ""}`);
|
|
214
235
|
}
|
|
215
236
|
|
|
216
|
-
// Canlı push (
|
|
217
|
-
// kazara canlıya basmasın diye açık onay şart (#431 L2).
|
|
237
|
+
// Canlı push (`--live`) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
|
|
238
|
+
// kazara canlıya basmasın diye açık onay şart (#431 L2). Draft/instance modu
|
|
239
|
+
// güvenli → otomatik onay (prompt yok).
|
|
218
240
|
const decision = livePushDecision({
|
|
219
|
-
draft:
|
|
241
|
+
draft: mode !== "live",
|
|
220
242
|
yes: Boolean(flags.yes),
|
|
221
243
|
confirm: Boolean(flags.confirm),
|
|
222
244
|
isTTY: Boolean(process.stdin.isTTY),
|
|
223
245
|
});
|
|
224
246
|
if (decision.mustAbort) {
|
|
225
|
-
console.error(`⚠ 'theme push' writes to the LIVE theme of ${target} immediately (no preview).`);
|
|
226
|
-
console.error(` Non-interactive shell: pass --yes to confirm, or
|
|
247
|
+
console.error(`⚠ 'theme push --live' writes to the LIVE theme of ${target} immediately (no preview).`);
|
|
248
|
+
console.error(` Non-interactive shell: pass --live --yes to confirm, or omit --live to push to a safe draft.`);
|
|
227
249
|
process.exit(1);
|
|
228
250
|
}
|
|
229
251
|
if (decision.needsPrompt) {
|
|
@@ -235,15 +257,40 @@ async function themePush(rest) {
|
|
|
235
257
|
rl.close();
|
|
236
258
|
}
|
|
237
259
|
if (!isAffirmative(answer)) {
|
|
238
|
-
console.error("Aborted.
|
|
260
|
+
console.error("Aborted. Omit `--live` to push to a safe draft, or pass `--live --yes` to confirm.");
|
|
239
261
|
process.exit(1);
|
|
240
262
|
}
|
|
241
263
|
}
|
|
242
264
|
|
|
243
|
-
|
|
265
|
+
// `--name` yalnızca YENİ taslak yaratırken (draft modu) anlamlı — canlıya/mevcut
|
|
266
|
+
// instance'a yazarken ad kaydedilmez, sessizce kaybolmasın diye açıkça uyar.
|
|
267
|
+
if (name && mode !== "draft") {
|
|
268
|
+
console.error("Note: --name yalnız yeni taslak yaratırken (varsayılan push) geçerli, yok sayıldı.");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result = await pushTheme({
|
|
272
|
+
dir,
|
|
273
|
+
url: creds.url,
|
|
274
|
+
token: creds.token,
|
|
275
|
+
draft: mode === "draft",
|
|
276
|
+
instance: mode === "instance" ? instance : null,
|
|
277
|
+
name: mode === "draft" ? name : null,
|
|
278
|
+
onRetry: (info) => console.error(retryNotice(info)),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Sunucu canlı-yazımı bildirdiyse (yeni alan; eski sunucuda yok) belirgin uyar.
|
|
282
|
+
if (result.warning === "live_write" && result.message) {
|
|
283
|
+
console.error(`\n⚠ ${result.message}\n`);
|
|
284
|
+
}
|
|
285
|
+
|
|
244
286
|
if (result.instanceId) {
|
|
245
287
|
console.log(`Pushed to theme ${result.instanceId} (${result.created} created, ${result.updated} updated).`);
|
|
246
|
-
|
|
288
|
+
if (mode === "draft") {
|
|
289
|
+
console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
|
|
290
|
+
console.log(`Publish it live with: blocofy theme publish`);
|
|
291
|
+
} else {
|
|
292
|
+
console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
|
|
293
|
+
}
|
|
247
294
|
} else {
|
|
248
295
|
const extra = result.skippedDeletes
|
|
249
296
|
? `, ${result.skippedDeletes} remote file(s) absent locally (not deleted)`
|
|
@@ -299,7 +346,8 @@ async function themeDev(rest) {
|
|
|
299
346
|
let session = null;
|
|
300
347
|
if (!flags["no-sync"]) {
|
|
301
348
|
try {
|
|
302
|
-
|
|
349
|
+
const name = typeof flags.name === "string" ? flags.name : null;
|
|
350
|
+
session = await fetchDevSession({ url: creds.url, token: creds.token, name });
|
|
303
351
|
} catch (error) {
|
|
304
352
|
console.warn(
|
|
305
353
|
`Warning: dev session unavailable (${error?.message ?? error}). ` +
|
|
@@ -366,6 +414,7 @@ async function themeDev(rest) {
|
|
|
366
414
|
token: creds.token,
|
|
367
415
|
port,
|
|
368
416
|
syncDraft: Boolean(session),
|
|
417
|
+
onRetry: (info) => console.error(` ${retryNotice(info)}`),
|
|
369
418
|
// Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
|
|
370
419
|
// = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
|
|
371
420
|
// hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
|
|
@@ -436,6 +485,20 @@ async function themePublish(rest) {
|
|
|
436
485
|
);
|
|
437
486
|
}
|
|
438
487
|
|
|
488
|
+
async function themeRename(rest) {
|
|
489
|
+
const { flags, positionals } = parseArgs(rest);
|
|
490
|
+
const handle = positionals[0];
|
|
491
|
+
const name = positionals.slice(1).join(" ") || (typeof flags.name === "string" ? flags.name : null);
|
|
492
|
+
if (!handle || !name) {
|
|
493
|
+
console.error("Usage: blocofy theme rename <handle> <new name>");
|
|
494
|
+
console.error(" Rename a theme (handle from the panel theme card or `blocofy status`).");
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
const creds = requireCreds();
|
|
498
|
+
const result = await renameInstance({ url: creds.url, token: creds.token, instance: handle, name });
|
|
499
|
+
console.log(`✓ Renamed to "${result.name}" (${result.id}).`);
|
|
500
|
+
}
|
|
501
|
+
|
|
439
502
|
async function status() {
|
|
440
503
|
const creds = requireCreds();
|
|
441
504
|
const s = await fetchSiteStatus({ url: creds.url, token: creds.token });
|
|
@@ -447,12 +510,23 @@ async function status() {
|
|
|
447
510
|
: `Live theme: none`,
|
|
448
511
|
);
|
|
449
512
|
console.log(`Health: ${s.health}`);
|
|
513
|
+
// orphan_missing_slugs yeni sunucu alanı (PR #661). Eski/deploy-edilmemiş sunucuda
|
|
514
|
+
// yok → null; o durumda eski (yumuşatılmış) sayaç satırına düş.
|
|
515
|
+
const missing = Array.isArray(s.orphan_missing_slugs) ? s.orphan_missing_slugs : null;
|
|
450
516
|
if (s.health === "live_instance_empty") {
|
|
451
517
|
console.error(
|
|
452
518
|
` ⚠ The live theme has no pages — the site will 404. Publish a theme with content: blocofy theme publish`,
|
|
453
519
|
);
|
|
454
520
|
} else if (s.health === "pages_split") {
|
|
455
|
-
|
|
521
|
+
if (missing && missing.length) {
|
|
522
|
+
console.warn(
|
|
523
|
+
` ⚠ These pages are published only on a non-live theme, so visitors can't reach them (404 risk): ` +
|
|
524
|
+
`${missing.join(", ")}\n` +
|
|
525
|
+
` Fix: publish the theme that has them — blocofy theme publish`,
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
console.warn(` ⚠ ${s.orphaned_pages} page(s) published on a non-live theme.`);
|
|
529
|
+
}
|
|
456
530
|
}
|
|
457
531
|
if (Array.isArray(s.drafts) && s.drafts.length > 0) {
|
|
458
532
|
console.log(`Drafts: ${s.drafts.map((d) => `${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
|
|
@@ -491,6 +565,11 @@ if (first === "--version" || first === "-v") {
|
|
|
491
565
|
console.error(error?.message ?? error);
|
|
492
566
|
process.exit(1);
|
|
493
567
|
});
|
|
568
|
+
} else if (first === "theme" && rest[0] === "rename") {
|
|
569
|
+
themeRename(rest.slice(1)).catch((error) => {
|
|
570
|
+
console.error(error?.message ?? error);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
});
|
|
494
573
|
} else if (first === "status") {
|
|
495
574
|
status().catch((error) => {
|
|
496
575
|
console.error(error?.message ?? error);
|
package/lib/args.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Sonraki token'ı değer olarak almayan flag'ler. */
|
|
8
|
-
const BOOLEAN_FLAGS = new Set(["draft", "yes", "confirm", "no-sync", "dry", "help", "version"]);
|
|
8
|
+
const BOOLEAN_FLAGS = new Set(["draft", "live", "yes", "confirm", "no-sync", "dry", "help", "version"]);
|
|
9
9
|
|
|
10
10
|
export function parseArgs(rest) {
|
|
11
11
|
const flags = {};
|
package/lib/confirm.mjs
CHANGED
|
@@ -15,6 +15,22 @@ export function livePushDecision({ draft, yes, confirm, isTTY }) {
|
|
|
15
15
|
return { autoApproved: false, needsPrompt: true, mustAbort: false };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* `theme push` hedefini seç (saf, yan etkisiz). Yeni varsayılan: DRAFT (güvenli).
|
|
20
|
+
* `--live` eski "anında canlı" davranışını açıkça geri getirir.
|
|
21
|
+
* - `instance` verilmişse → { mode: "instance", instance } (açık-hedef; `--live`
|
|
22
|
+
* olsa bile instance KAZANIR — kullanıcı belirli bir temayı adresledi).
|
|
23
|
+
* - `--live` (instance yok) → { mode: "live" } (onay gerektiren tek yol).
|
|
24
|
+
* - aksi halde → { mode: "draft" } (varsayılan; açık `--draft` dahil).
|
|
25
|
+
* - `--live` + `--draft` birlikte → draft (güvenli olan kazanır).
|
|
26
|
+
*/
|
|
27
|
+
export function resolvePushMode({ live, draft, instance } = {}) {
|
|
28
|
+
if (instance) return { mode: "instance", instance };
|
|
29
|
+
if (draft) return { mode: "draft" };
|
|
30
|
+
if (live) return { mode: "live" };
|
|
31
|
+
return { mode: "draft" };
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
/** y/N yanıtını yorumla (yes/y → onay). */
|
|
19
35
|
export function isAffirmative(answer) {
|
|
20
36
|
const a = String(answer ?? "").trim().toLowerCase();
|
package/lib/dev-server.mjs
CHANGED
|
@@ -20,7 +20,7 @@ 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 }) {
|
|
23
|
+
export function startDevServer({ dir, url, token, port, syncDraft = false, onError, onReload, onRetry }) {
|
|
24
24
|
const base = url.replace(/\/+$/, "");
|
|
25
25
|
const clients = new Set();
|
|
26
26
|
|
|
@@ -97,7 +97,7 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
97
97
|
let error = null;
|
|
98
98
|
if (syncDraft) {
|
|
99
99
|
try {
|
|
100
|
-
await pushTheme({ dir, url, token, draft: true });
|
|
100
|
+
await pushTheme({ dir, url, token, draft: true, onRetry });
|
|
101
101
|
} catch (err) {
|
|
102
102
|
error = err?.message ?? String(err); // senkron hatası reload'u engellemesin
|
|
103
103
|
}
|
|
@@ -108,7 +108,7 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
108
108
|
|
|
109
109
|
// Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
|
|
110
110
|
if (syncDraft) {
|
|
111
|
-
void pushTheme({ dir, url, token, draft: true }).catch(() => {});
|
|
111
|
+
void pushTheme({ dir, url, token, draft: true, onRetry }).catch(() => {});
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
server.listen(port);
|
package/lib/local-theme.mjs
CHANGED
|
@@ -31,7 +31,10 @@ export function readLocalTemplates(dir) {
|
|
|
31
31
|
}
|
|
32
32
|
const rel = relative(dir, full).split(sep).join("/");
|
|
33
33
|
const top = rel.split("/")[0] ?? "";
|
|
34
|
-
|
|
34
|
+
// config/ dizininden YALNIZ settings_schema.json senkronlanır (tema-ayarları
|
|
35
|
+
// şeması); color_schemes.json/theme.json kendi sistemlerinde. Platform tarafıyla
|
|
36
|
+
// (github-sync mapRepoPath / theme-write themeWriteKind) simetrik tek-path istisnası.
|
|
37
|
+
if (rel !== "config/settings_schema.json" && !THEME_DIRS.has(top)) continue;
|
|
35
38
|
out[rel.endsWith(".liquid") ? rel.slice(0, -7) : rel] = readFileSync(full, "utf8");
|
|
36
39
|
}
|
|
37
40
|
};
|
package/lib/messages.mjs
CHANGED
|
@@ -14,8 +14,8 @@ import { THEME_DIRS } from "./local-theme.mjs";
|
|
|
14
14
|
export function syncScopeNote() {
|
|
15
15
|
const synced = [...THEME_DIRS].sort().map((d) => `${d}/`).join(" ");
|
|
16
16
|
return [
|
|
17
|
-
`Synced (theme code): ${synced}`,
|
|
18
|
-
`Not synced (cloud):
|
|
17
|
+
`Synced (theme code): ${synced} config/settings_schema.json`,
|
|
18
|
+
`Not synced (cloud): pages/ (other config/ files) — edit these in the admin editor`,
|
|
19
19
|
];
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -31,6 +31,16 @@ export function statusLine(session) {
|
|
|
31
31
|
return `Local files → Draft theme ${draft} · Live theme → ${live}`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Geçici ağ hatasında yeniden-deneme bildirimi (saf → test edilebilir). `push`
|
|
36
|
+
* (ve `theme dev` taslak senkronu) sessizce retry ETMESİN; stderr'e kısa bir satır
|
|
37
|
+
* yazılır. `onRetry({ attempt, retries, reason })` payload'ından string üretir.
|
|
38
|
+
*/
|
|
39
|
+
export function retryNotice({ attempt, retries, reason }) {
|
|
40
|
+
const why = reason ? ` (${reason})` : "";
|
|
41
|
+
return `Network error${why} — retrying (${attempt}/${retries})…`;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* Dev oturumuna göre "düzlem" uyarısı döndürür (string), oturum yoksa null.
|
|
36
46
|
* - GitHub bağlı → repo@branch + `git pull` ipucu.
|
package/lib/theme-sync.mjs
CHANGED
|
@@ -18,6 +18,49 @@ async function errorText(res) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Backoff delays (ms) between attempts — index 0 = after the first failure. */
|
|
22
|
+
const RETRY_BACKOFF_MS = [300, 900];
|
|
23
|
+
|
|
24
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* fetch wrapper that retries ONLY on transient failures: a thrown fetch (network
|
|
28
|
+
* error / connection reset) or a 5xx / 429 response. Permanent 4xx (401/403/422 …)
|
|
29
|
+
* fail immediately — no retry. Max 2 retries with a short backoff (300ms, 900ms).
|
|
30
|
+
*
|
|
31
|
+
* Safe because every endpoint here is idempotent: theme push is a file upsert
|
|
32
|
+
* (create/update, no delete), pull/whoami/session/site are reads, publish sets a
|
|
33
|
+
* pointer. Re-sending the same request cannot double-apply anything.
|
|
34
|
+
*
|
|
35
|
+
* `onRetry({ attempt, retries, reason })` is called before each wait so callers can
|
|
36
|
+
* surface a "Network error, retrying…" line (not silent).
|
|
37
|
+
*/
|
|
38
|
+
export async function fetchWithRetry(input, init, { retries = 2, onRetry, backoff = RETRY_BACKOFF_MS } = {}) {
|
|
39
|
+
let lastErr;
|
|
40
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(input, init);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Thrown fetch = network-level failure (DNS, reset, "fetch failed"). Transient.
|
|
46
|
+
lastErr = err;
|
|
47
|
+
if (attempt === retries) throw err;
|
|
48
|
+
onRetry?.({ attempt: attempt + 1, retries, reason: err?.message ?? String(err) });
|
|
49
|
+
await sleep(backoff[attempt] ?? 0);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// 5xx / 429 = transient server-side; retry. Other statuses (incl. 4xx) return as-is.
|
|
53
|
+
if ((res.status >= 500 || res.status === 429) && attempt < retries) {
|
|
54
|
+
onRetry?.({ attempt: attempt + 1, retries, reason: `HTTP ${res.status}` });
|
|
55
|
+
await sleep(backoff[attempt] ?? 0);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
// Unreachable (loop returns or throws), but keeps the type honest.
|
|
61
|
+
throw lastErr;
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
/**
|
|
22
65
|
* Download a theme to disk. `{ path: content }` (stripped) → `.liquid` files.
|
|
23
66
|
* With `draft`, pulls the "CLI Draft" instance (what `theme dev` syncs into)
|
|
@@ -60,9 +103,10 @@ export async function fetchWhoami({ url, token }) {
|
|
|
60
103
|
* Dev session bilgisi (#119 `theme dev`): platform draft instance'ı hazırlar ve
|
|
61
104
|
* 3 görünümün URL'lerini döner — `{ draftInstanceId, previewUrl, editorUrl, site }`.
|
|
62
105
|
*/
|
|
63
|
-
export async function fetchDevSession({ url, token }) {
|
|
106
|
+
export async function fetchDevSession({ url, token, name = null }) {
|
|
64
107
|
const base = url.replace(/\/+$/, "");
|
|
65
|
-
const
|
|
108
|
+
const query = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
109
|
+
const res = await fetch(`${base}/api/dev/session${query}`, {
|
|
66
110
|
headers: { authorization: `Bearer ${token}` },
|
|
67
111
|
});
|
|
68
112
|
if (!res.ok) throw new Error(await errorText(res));
|
|
@@ -74,15 +118,19 @@ export async function fetchDevSession({ url, token }) {
|
|
|
74
118
|
* writes to a draft theme instance instead of the live theme — preview & publish
|
|
75
119
|
* it from the admin panel without affecting the live site.
|
|
76
120
|
*/
|
|
77
|
-
export async function pushTheme({ dir, url, token, draft = false, instance = null }) {
|
|
121
|
+
export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, onRetry = null }) {
|
|
78
122
|
const base = url.replace(/\/+$/, "");
|
|
79
123
|
const files = readLocalTemplates(dir);
|
|
80
|
-
const payload = instance ? { files, instance } : { files, draft };
|
|
81
|
-
const res = await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
const payload = instance ? { files, instance } : name ? { files, draft, name } : { files, draft };
|
|
125
|
+
const res = await fetchWithRetry(
|
|
126
|
+
`${base}/api/dev/theme`,
|
|
127
|
+
{
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
130
|
+
body: JSON.stringify(payload),
|
|
131
|
+
},
|
|
132
|
+
{ onRetry },
|
|
133
|
+
);
|
|
86
134
|
if (!res.ok) throw new Error(await errorText(res));
|
|
87
135
|
return res.json();
|
|
88
136
|
}
|
|
@@ -103,6 +151,22 @@ export async function publishInstance({ url, token, instanceId }) {
|
|
|
103
151
|
return res.json();
|
|
104
152
|
}
|
|
105
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Bir tema instance'ının adını değiştir (`POST /api/dev/theme/rename`). Ad yalnızca
|
|
156
|
+
* bir etiket — canlı instance dahil sahip olunan her instance yeniden adlandırılabilir.
|
|
157
|
+
* `{ ok, id, name }` döner.
|
|
158
|
+
*/
|
|
159
|
+
export async function renameInstance({ url, token, instance, name }) {
|
|
160
|
+
const base = url.replace(/\/+$/, "");
|
|
161
|
+
const res = await fetch(`${base}/api/dev/theme/rename`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
164
|
+
body: JSON.stringify({ instance, name }),
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) throw new Error(await errorText(res));
|
|
167
|
+
return res.json();
|
|
168
|
+
}
|
|
169
|
+
|
|
106
170
|
/**
|
|
107
171
|
* Site sağlık/durum özeti (`GET /api/dev/site`) — `blocofy status`. Canlı tema instance'ı,
|
|
108
172
|
* instance-başına sayfa dağılımı, taslaklar ve health döner.
|