@blocofy/cli 0.2.3 → 0.2.4
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 +1 -0
- package/bin/blocofy.mjs +10 -2
- 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 +53 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ A theme is a directory of files grouped by top-level folder:
|
|
|
53
53
|
| `asset/` | CSS and static files (`theme.css`) |
|
|
54
54
|
| `pages/<slug>.json` | Page content (`/` → `pages/index.json`) |
|
|
55
55
|
| `config/settings.json` | Theme settings + color schemes |
|
|
56
|
+
| `config/settings_schema.json` | Theme settings panel schema (synced with `pull`/`push`) |
|
|
56
57
|
|
|
57
58
|
Liquid templates use the `.liquid` extension; files under `asset/` are served as-is.
|
|
58
59
|
|
package/bin/blocofy.mjs
CHANGED
|
@@ -18,7 +18,7 @@ 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";
|
|
21
|
+
import { githubNote, retryNotice, statusLine, syncScopeNote } from "../lib/messages.mjs";
|
|
22
22
|
import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
23
23
|
import { isAffirmative, livePushDecision } from "../lib/confirm.mjs";
|
|
24
24
|
import { hyperlink, openUrl } from "../lib/term.mjs";
|
|
@@ -240,7 +240,14 @@ async function themePush(rest) {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
const result = await pushTheme({
|
|
243
|
+
const result = await pushTheme({
|
|
244
|
+
dir,
|
|
245
|
+
url: creds.url,
|
|
246
|
+
token: creds.token,
|
|
247
|
+
draft,
|
|
248
|
+
instance,
|
|
249
|
+
onRetry: (info) => console.error(retryNotice(info)),
|
|
250
|
+
});
|
|
244
251
|
if (result.instanceId) {
|
|
245
252
|
console.log(`Pushed to theme ${result.instanceId} (${result.created} created, ${result.updated} updated).`);
|
|
246
253
|
console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
|
|
@@ -366,6 +373,7 @@ async function themeDev(rest) {
|
|
|
366
373
|
token: creds.token,
|
|
367
374
|
port,
|
|
368
375
|
syncDraft: Boolean(session),
|
|
376
|
+
onRetry: (info) => console.error(` ${retryNotice(info)}`),
|
|
369
377
|
// Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
|
|
370
378
|
// = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
|
|
371
379
|
// hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
|
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)
|
|
@@ -74,15 +117,19 @@ export async function fetchDevSession({ url, token }) {
|
|
|
74
117
|
* writes to a draft theme instance instead of the live theme — preview & publish
|
|
75
118
|
* it from the admin panel without affecting the live site.
|
|
76
119
|
*/
|
|
77
|
-
export async function pushTheme({ dir, url, token, draft = false, instance = null }) {
|
|
120
|
+
export async function pushTheme({ dir, url, token, draft = false, instance = null, onRetry = null }) {
|
|
78
121
|
const base = url.replace(/\/+$/, "");
|
|
79
122
|
const files = readLocalTemplates(dir);
|
|
80
123
|
const payload = instance ? { files, instance } : { files, draft };
|
|
81
|
-
const res = await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
const res = await fetchWithRetry(
|
|
125
|
+
`${base}/api/dev/theme`,
|
|
126
|
+
{
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
129
|
+
body: JSON.stringify(payload),
|
|
130
|
+
},
|
|
131
|
+
{ onRetry },
|
|
132
|
+
);
|
|
86
133
|
if (!res.ok) throw new Error(await errorText(res));
|
|
87
134
|
return res.json();
|
|
88
135
|
}
|