@blocofy/cli 0.2.4 → 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 +18 -2
- package/bin/blocofy.mjs +96 -25
- package/lib/args.mjs +1 -1
- package/lib/confirm.mjs +16 -0
- package/lib/theme-sync.mjs +21 -4
- 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
|
package/bin/blocofy.mjs
CHANGED
|
@@ -19,8 +19,8 @@ import { credentialsPath, loadCredentials, saveCredentials } from "../lib/creden
|
|
|
19
19
|
import { startDevServer } from "../lib/dev-server.mjs";
|
|
20
20
|
import { readLocalTemplates } from "../lib/local-theme.mjs";
|
|
21
21
|
import { githubNote, retryNotice, 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";
|
|
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,22 +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
|
|
|
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
|
+
|
|
243
271
|
const result = await pushTheme({
|
|
244
272
|
dir,
|
|
245
273
|
url: creds.url,
|
|
246
274
|
token: creds.token,
|
|
247
|
-
draft,
|
|
248
|
-
instance,
|
|
275
|
+
draft: mode === "draft",
|
|
276
|
+
instance: mode === "instance" ? instance : null,
|
|
277
|
+
name: mode === "draft" ? name : null,
|
|
249
278
|
onRetry: (info) => console.error(retryNotice(info)),
|
|
250
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
|
+
|
|
251
286
|
if (result.instanceId) {
|
|
252
287
|
console.log(`Pushed to theme ${result.instanceId} (${result.created} created, ${result.updated} updated).`);
|
|
253
|
-
|
|
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
|
+
}
|
|
254
294
|
} else {
|
|
255
295
|
const extra = result.skippedDeletes
|
|
256
296
|
? `, ${result.skippedDeletes} remote file(s) absent locally (not deleted)`
|
|
@@ -306,7 +346,8 @@ async function themeDev(rest) {
|
|
|
306
346
|
let session = null;
|
|
307
347
|
if (!flags["no-sync"]) {
|
|
308
348
|
try {
|
|
309
|
-
|
|
349
|
+
const name = typeof flags.name === "string" ? flags.name : null;
|
|
350
|
+
session = await fetchDevSession({ url: creds.url, token: creds.token, name });
|
|
310
351
|
} catch (error) {
|
|
311
352
|
console.warn(
|
|
312
353
|
`Warning: dev session unavailable (${error?.message ?? error}). ` +
|
|
@@ -444,6 +485,20 @@ async function themePublish(rest) {
|
|
|
444
485
|
);
|
|
445
486
|
}
|
|
446
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
|
+
|
|
447
502
|
async function status() {
|
|
448
503
|
const creds = requireCreds();
|
|
449
504
|
const s = await fetchSiteStatus({ url: creds.url, token: creds.token });
|
|
@@ -455,12 +510,23 @@ async function status() {
|
|
|
455
510
|
: `Live theme: none`,
|
|
456
511
|
);
|
|
457
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;
|
|
458
516
|
if (s.health === "live_instance_empty") {
|
|
459
517
|
console.error(
|
|
460
518
|
` ⚠ The live theme has no pages — the site will 404. Publish a theme with content: blocofy theme publish`,
|
|
461
519
|
);
|
|
462
520
|
} else if (s.health === "pages_split") {
|
|
463
|
-
|
|
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
|
+
}
|
|
464
530
|
}
|
|
465
531
|
if (Array.isArray(s.drafts) && s.drafts.length > 0) {
|
|
466
532
|
console.log(`Drafts: ${s.drafts.map((d) => `${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
|
|
@@ -499,6 +565,11 @@ if (first === "--version" || first === "-v") {
|
|
|
499
565
|
console.error(error?.message ?? error);
|
|
500
566
|
process.exit(1);
|
|
501
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
|
+
});
|
|
502
573
|
} else if (first === "status") {
|
|
503
574
|
status().catch((error) => {
|
|
504
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/theme-sync.mjs
CHANGED
|
@@ -103,9 +103,10 @@ export async function fetchWhoami({ url, token }) {
|
|
|
103
103
|
* Dev session bilgisi (#119 `theme dev`): platform draft instance'ı hazırlar ve
|
|
104
104
|
* 3 görünümün URL'lerini döner — `{ draftInstanceId, previewUrl, editorUrl, site }`.
|
|
105
105
|
*/
|
|
106
|
-
export async function fetchDevSession({ url, token }) {
|
|
106
|
+
export async function fetchDevSession({ url, token, name = null }) {
|
|
107
107
|
const base = url.replace(/\/+$/, "");
|
|
108
|
-
const
|
|
108
|
+
const query = name ? `?name=${encodeURIComponent(name)}` : "";
|
|
109
|
+
const res = await fetch(`${base}/api/dev/session${query}`, {
|
|
109
110
|
headers: { authorization: `Bearer ${token}` },
|
|
110
111
|
});
|
|
111
112
|
if (!res.ok) throw new Error(await errorText(res));
|
|
@@ -117,10 +118,10 @@ export async function fetchDevSession({ url, token }) {
|
|
|
117
118
|
* writes to a draft theme instance instead of the live theme — preview & publish
|
|
118
119
|
* it from the admin panel without affecting the live site.
|
|
119
120
|
*/
|
|
120
|
-
export async function pushTheme({ dir, url, token, draft = false, instance = null, onRetry = null }) {
|
|
121
|
+
export async function pushTheme({ dir, url, token, draft = false, instance = null, name = null, onRetry = null }) {
|
|
121
122
|
const base = url.replace(/\/+$/, "");
|
|
122
123
|
const files = readLocalTemplates(dir);
|
|
123
|
-
const payload = instance ? { files, instance } : { files, draft };
|
|
124
|
+
const payload = instance ? { files, instance } : name ? { files, draft, name } : { files, draft };
|
|
124
125
|
const res = await fetchWithRetry(
|
|
125
126
|
`${base}/api/dev/theme`,
|
|
126
127
|
{
|
|
@@ -150,6 +151,22 @@ export async function publishInstance({ url, token, instanceId }) {
|
|
|
150
151
|
return res.json();
|
|
151
152
|
}
|
|
152
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
|
+
|
|
153
170
|
/**
|
|
154
171
|
* Site sağlık/durum özeti (`GET /api/dev/site`) — `blocofy status`. Canlı tema instance'ı,
|
|
155
172
|
* instance-başına sayfa dağılımı, taslaklar ve health döner.
|