@blocofy/cli 0.1.3 → 0.1.5
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/bin/blocofy.mjs +140 -33
- package/lib/dev-server.mjs +12 -7
- package/lib/term.mjs +36 -0
- package/lib/validate.mjs +18 -0
- package/package.json +1 -1
package/bin/blocofy.mjs
CHANGED
|
@@ -14,9 +14,12 @@ import { createInterface } from "node:readline/promises";
|
|
|
14
14
|
|
|
15
15
|
import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
|
|
16
16
|
import { startDevServer } from "../lib/dev-server.mjs";
|
|
17
|
+
import { readLocalTemplates } from "../lib/local-theme.mjs";
|
|
17
18
|
import { fetchDevSession, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
19
|
+
import { hyperlink, openUrl } from "../lib/term.mjs";
|
|
20
|
+
import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
|
|
18
21
|
|
|
19
|
-
const VERSION = "0.1.
|
|
22
|
+
const VERSION = "0.1.5";
|
|
20
23
|
const args = process.argv.slice(2);
|
|
21
24
|
|
|
22
25
|
/** Parse `--key value` and `--flag` (boolean) arguments. */
|
|
@@ -40,51 +43,97 @@ function parseFlags(rest) {
|
|
|
40
43
|
function printHelp() {
|
|
41
44
|
console.log(`blocofy — Blocofy theme development CLI (v${VERSION})
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
Develop your theme locally against live data, preview it three ways, and publish.
|
|
47
|
+
|
|
48
|
+
Usage
|
|
44
49
|
blocofy login [--url <url>] [--token <bcf_…>]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
Save your platform URL + dev token to ~/.blocofy/credentials.json.
|
|
51
|
+
Get a token from the admin panel → Settings → Theme CLI tokens.
|
|
52
|
+
|
|
53
|
+
blocofy theme dev [dir] [--port <n>] [--no-sync]
|
|
54
|
+
Start a dev server and print 3 auto-reloading views — Local, live-domain
|
|
55
|
+
Preview, and the theme Editor. Press l / p / e to open each, q to quit.
|
|
56
|
+
Edit a file and save → every open view reloads. (dir defaults to cwd)
|
|
57
|
+
--port <n> local port (default 3030)
|
|
58
|
+
--no-sync local preview only (skip draft sync + remote views)
|
|
59
|
+
|
|
60
|
+
blocofy theme pull [dir]
|
|
61
|
+
Download the live theme to disk. (dir defaults to cwd)
|
|
62
|
+
|
|
63
|
+
blocofy theme push [dir] [--draft]
|
|
64
|
+
Write the local theme to the site (create/update; no delete).
|
|
65
|
+
--draft write to a draft theme — preview & publish it from the admin panel
|
|
66
|
+
|
|
53
67
|
blocofy --version
|
|
54
68
|
blocofy --help
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
Examples
|
|
71
|
+
blocofy login --url https://store.myblocofy.com --token bcf_xxxxxxxx
|
|
72
|
+
blocofy theme pull && blocofy theme dev
|
|
73
|
+
blocofy theme push --draft
|
|
74
|
+
|
|
75
|
+
Auth: ~/.blocofy/credentials.json (from \`login\`), or BLOCOFY_URL + BLOCOFY_TOKEN env vars.
|
|
76
|
+
The CLI does not build assets — bring your own (npm/Vite/Tailwind); the platform serves
|
|
77
|
+
plain Liquid + static assets.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Geçerli yanıt alana kadar sor (max `tries`), HER yanıtı anında doğrula. */
|
|
81
|
+
async function promptValid(rl, question, normalize, valid, hint, tries = 3) {
|
|
82
|
+
for (let i = 0; i < tries; i++) {
|
|
83
|
+
const answer = normalize(await rl.question(question));
|
|
84
|
+
if (valid(answer)) return answer;
|
|
85
|
+
console.error(` ✗ ${hint}`);
|
|
86
|
+
}
|
|
87
|
+
console.error("Too many invalid attempts.");
|
|
88
|
+
process.exit(1);
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
async function login(rest) {
|
|
61
92
|
const flags = parseFlags(rest);
|
|
62
|
-
let url = typeof flags.url === "string" ? flags.url : "";
|
|
63
|
-
let token = typeof flags.token === "string" ? flags.token : "";
|
|
93
|
+
let url = typeof flags.url === "string" ? normalizeUrl(flags.url) : "";
|
|
94
|
+
let token = typeof flags.token === "string" ? flags.token.trim() : "";
|
|
95
|
+
|
|
96
|
+
// Flag ile verildiyse anında doğrula (prompt'a düşmeden).
|
|
97
|
+
if (url && !isValidUrl(url)) {
|
|
98
|
+
console.error("Invalid --url — must be a valid http(s):// URL.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (token && !isValidToken(token)) {
|
|
102
|
+
console.error("Invalid --token — must start with bcf_.");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
64
105
|
|
|
65
106
|
if (!url || !token) {
|
|
66
107
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
67
108
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
109
|
+
// URL'i token'dan ÖNCE iste ve ANINDA doğrula (şema yoksa https:// eklenir);
|
|
110
|
+
// geçersizse aynı anda tekrar sorar — token'a geçip sonra hata vermez.
|
|
111
|
+
if (!url) {
|
|
112
|
+
url = await promptValid(
|
|
113
|
+
rl,
|
|
114
|
+
"Platform/site URL (e.g. https://store.myblocofy.com): ",
|
|
115
|
+
normalizeUrl,
|
|
116
|
+
isValidUrl,
|
|
117
|
+
"Enter a valid URL, e.g. https://store.myblocofy.com",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!token) {
|
|
121
|
+
token = await promptValid(
|
|
122
|
+
rl,
|
|
123
|
+
"Dev token (bcf_…): ",
|
|
124
|
+
(s) => s.trim(),
|
|
125
|
+
isValidToken,
|
|
126
|
+
"Token must start with bcf_ — get one from the admin panel: Settings → Theme CLI tokens.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
70
129
|
} finally {
|
|
71
130
|
rl.close();
|
|
72
131
|
}
|
|
73
132
|
}
|
|
74
133
|
|
|
75
|
-
url = url.replace(/\/+$/, "");
|
|
76
|
-
if (!/^https?:\/\//.test(url)) {
|
|
77
|
-
console.error("Invalid URL — must start with http(s)://");
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
if (!token.startsWith("bcf_")) {
|
|
81
|
-
console.error("Invalid token — must start with bcf_ (admin panel: Settings → Theme CLI tokens).");
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
134
|
saveCredentials({ url, token });
|
|
86
|
-
console.log(
|
|
87
|
-
console.log(`
|
|
135
|
+
console.log(`✓ Saved credentials → ${credentialsPath()}`);
|
|
136
|
+
console.log(`Next: cd into your theme directory, then run blocofy theme dev`);
|
|
88
137
|
}
|
|
89
138
|
|
|
90
139
|
/** Resolve credentials (URL + token) or exit with guidance. */
|
|
@@ -154,13 +203,33 @@ async function themeDev(rest) {
|
|
|
154
203
|
}
|
|
155
204
|
}
|
|
156
205
|
|
|
206
|
+
const localUrl = `http://localhost:${port}`;
|
|
207
|
+
const previewUrl = session ? `${session.previewUrl}&hr=${port}` : null;
|
|
208
|
+
const editorUrl = session ? `${session.editorUrl}&hr=${port}` : null;
|
|
209
|
+
|
|
157
210
|
console.log(`\nblocofy theme dev — ${creds.url} (token ${creds.token.slice(0, 8)}…)\n`);
|
|
158
|
-
console.log(` Local
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
211
|
+
console.log(` (l) Local ${hyperlink(localUrl)}`);
|
|
212
|
+
if (previewUrl) console.log(` (p) Preview ${hyperlink(previewUrl)}`);
|
|
213
|
+
if (editorUrl) console.log(` (e) Editor ${hyperlink(editorUrl)}`);
|
|
214
|
+
const keys = ["l local", previewUrl && "p preview", editorUrl && "e editor", "q quit"]
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
.join(" ");
|
|
217
|
+
console.log(`\n Press: ${keys}`);
|
|
218
|
+
console.log(` Edit a theme file and save — every open view reloads automatically.\n`);
|
|
219
|
+
|
|
220
|
+
// Tanılama: kaç tema dosyası izleniyor? Boşsa hot-reload mümkün değil — yanlış
|
|
221
|
+
// dizinde ya da `theme pull` yapılmamış demektir; sebebini açıkça söyle.
|
|
222
|
+
const localFiles = readLocalTemplates(themeDir);
|
|
223
|
+
const fileCount = Object.keys(localFiles).length;
|
|
224
|
+
if (fileCount === 0) {
|
|
225
|
+
console.warn(
|
|
226
|
+
` ⚠ No theme files found under ${themeDir}\n` +
|
|
227
|
+
` Expected top-level folders: layout/ section/ partial/ asset/ block/ template/\n` +
|
|
228
|
+
` Run this from your theme root, or fetch it first: blocofy theme pull\n`,
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` Watching ${themeDir} — ${fileCount} theme files\n`);
|
|
162
232
|
}
|
|
163
|
-
console.log(`\n Edit a theme file and save — every open view reloads automatically.\n`);
|
|
164
233
|
|
|
165
234
|
if (flags.dry) {
|
|
166
235
|
console.log("(--dry: server not started)");
|
|
@@ -173,6 +242,18 @@ async function themeDev(rest) {
|
|
|
173
242
|
token: creds.token,
|
|
174
243
|
port,
|
|
175
244
|
syncDraft: Boolean(session),
|
|
245
|
+
// Her kaydetmede ne olduğunu bas — "reloaded" = watch tetiklendi; "0 views"
|
|
246
|
+
// = hiçbir tarayıcı sekmesi bağlı değil (yanlış görünüme bakıyorsun); sync
|
|
247
|
+
// hatası = draft güncellenemedi (preview/editör eski kalır, local yine yenilenir).
|
|
248
|
+
onReload: ({ file, synced, clients, error }) => {
|
|
249
|
+
const what = file || "change";
|
|
250
|
+
if (error) {
|
|
251
|
+
console.error(` ↻ ${what} — draft sync failed: ${error} (local view still reloaded)`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const views = clients ? `${clients} view${clients === 1 ? "" : "s"}` : "no views connected";
|
|
255
|
+
console.log(` ↻ ${what} → ${synced ? "synced + " : ""}reloaded (${views})`);
|
|
256
|
+
},
|
|
176
257
|
onError: (err) => {
|
|
177
258
|
if (err && err.code === "EADDRINUSE") {
|
|
178
259
|
console.error(`Port ${port} is in use. Try a different port: blocofy theme dev --port <n>`);
|
|
@@ -183,11 +264,37 @@ async function themeDev(rest) {
|
|
|
183
264
|
},
|
|
184
265
|
});
|
|
185
266
|
const shutdown = () => {
|
|
267
|
+
if (process.stdin.isTTY) {
|
|
268
|
+
try {
|
|
269
|
+
process.stdin.setRawMode(false);
|
|
270
|
+
} catch {
|
|
271
|
+
/* yoksay */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
186
274
|
handle.close();
|
|
187
275
|
process.exit(0);
|
|
188
276
|
};
|
|
189
277
|
process.on("SIGINT", shutdown);
|
|
190
278
|
process.on("SIGTERM", shutdown);
|
|
279
|
+
|
|
280
|
+
// Klavye kısayolları (TTY): l/p/e ilgili görünümü tarayıcıda açar, q/Ctrl-C çıkar.
|
|
281
|
+
// Raw mode'da SIGINT gelmez → Ctrl-C'yi () elle yakala.
|
|
282
|
+
if (process.stdin.isTTY) {
|
|
283
|
+
try {
|
|
284
|
+
process.stdin.setRawMode(true);
|
|
285
|
+
} catch {
|
|
286
|
+
return; // raw mode yoksa kısayolsuz devam (sunucu çalışmaya devam eder)
|
|
287
|
+
}
|
|
288
|
+
process.stdin.resume();
|
|
289
|
+
process.stdin.setEncoding("utf8");
|
|
290
|
+
process.stdin.on("data", (key) => {
|
|
291
|
+
const k = key.toLowerCase();
|
|
292
|
+
if (key === "" || k === "q") shutdown();
|
|
293
|
+
else if (k === "l") openUrl(localUrl);
|
|
294
|
+
else if (k === "p" && previewUrl) openUrl(previewUrl);
|
|
295
|
+
else if (k === "e" && editorUrl) openUrl(editorUrl);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
191
298
|
}
|
|
192
299
|
|
|
193
300
|
const [first, ...rest] = args;
|
package/lib/dev-server.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { pushTheme } from "./theme-sync.mjs";
|
|
|
19
19
|
* separate request); direct `/asset/*` references are served from disk. Images/JS
|
|
20
20
|
* are remote (Directus/CDN), so no local serving is needed for them.
|
|
21
21
|
*/
|
|
22
|
-
export function startDevServer({ dir, url, token, port, syncDraft = false, onError }) {
|
|
22
|
+
export function startDevServer({ dir, url, token, port, syncDraft = false, onError, onReload }) {
|
|
23
23
|
const base = url.replace(/\/+$/, "");
|
|
24
24
|
const clients = new Set();
|
|
25
25
|
|
|
@@ -90,16 +90,19 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
90
90
|
for (const client of clients) client.write("data: reload\n\n");
|
|
91
91
|
};
|
|
92
92
|
// Değişimde: önce taslağa senkronla (canlı-domain önizleme + editör güncel olsun),
|
|
93
|
-
// SONRA reload yayınla → tüm görünümler en güncel taslağı çeker.
|
|
94
|
-
|
|
93
|
+
// SONRA reload yayınla → tüm görünümler en güncel taslağı çeker. `onReload` log için:
|
|
94
|
+
// hangi dosya, senkron oldu mu, kaç görünüm bağlı (clients.size = açık EventSource'lar).
|
|
95
|
+
const watcher = startWatch(dir, async (file) => {
|
|
96
|
+
let error = null;
|
|
95
97
|
if (syncDraft) {
|
|
96
98
|
try {
|
|
97
99
|
await pushTheme({ dir, url, token, draft: true });
|
|
98
|
-
} catch {
|
|
99
|
-
|
|
100
|
+
} catch (err) {
|
|
101
|
+
error = err?.message ?? String(err); // senkron hatası reload'u engellemesin
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
broadcast();
|
|
105
|
+
onReload?.({ file, synced: syncDraft && !error, clients: clients.size, error });
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
// Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
|
|
@@ -130,9 +133,11 @@ export function startDevServer({ dir, url, token, port, syncDraft = false, onErr
|
|
|
130
133
|
*/
|
|
131
134
|
function startWatch(dir, onChange) {
|
|
132
135
|
let timer = null;
|
|
133
|
-
|
|
136
|
+
let lastFile = null;
|
|
137
|
+
const fire = (_event, filename) => {
|
|
138
|
+
if (filename) lastFile = String(filename).split(sep).join("/");
|
|
134
139
|
if (timer) clearTimeout(timer);
|
|
135
|
-
timer = setTimeout(onChange, 120);
|
|
140
|
+
timer = setTimeout(() => onChange(lastFile), 120);
|
|
136
141
|
};
|
|
137
142
|
try {
|
|
138
143
|
return watch(dir, { recursive: true }, fire);
|
package/lib/term.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal yardımcıları: URL'leri varsayılan tarayıcıda açma + tıklanabilir
|
|
5
|
+
* (OSC 8) hyperlink. `theme dev` 3 görünümü tıklanabilir + l/p/e kısayollarıyla açar.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Platforma göre URL açma komutu (saf → test edilebilir). */
|
|
9
|
+
export function openCommand(url, platform = process.platform) {
|
|
10
|
+
if (platform === "darwin") return { cmd: "open", args: [url] };
|
|
11
|
+
if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", "", url] };
|
|
12
|
+
return { cmd: "xdg-open", args: [url] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** URL'i varsayılan tarayıcıda aç (detached, sessiz — opener yoksa yutulur). */
|
|
16
|
+
export function openUrl(url) {
|
|
17
|
+
const { cmd, args } = openCommand(url);
|
|
18
|
+
try {
|
|
19
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
20
|
+
child.on("error", () => {});
|
|
21
|
+
child.unref();
|
|
22
|
+
} catch {
|
|
23
|
+
/* opener yok → sessiz */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const OSC = "]8;;";
|
|
28
|
+
const BEL = "";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* OSC 8 terminal hyperlink: destekleyen terminalde (iTerm2, GNOME Terminal, …)
|
|
32
|
+
* tıklanabilir; desteklemeyende `label` düz metin olarak görünür.
|
|
33
|
+
*/
|
|
34
|
+
export function hyperlink(url, label = url) {
|
|
35
|
+
return `${OSC}${url}${BEL}${label}${OSC}${BEL}`;
|
|
36
|
+
}
|
package/lib/validate.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login girdisi doğrulama (saf → test edilebilir). URL şema yoksa https:// eklenir
|
|
3
|
+
* (bare domain'e izin), sondaki `/` atılır; token `bcf_` ile başlamalı.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function normalizeUrl(raw) {
|
|
7
|
+
let u = (raw || "").trim().replace(/\/+$/, "");
|
|
8
|
+
if (u && !/^https?:\/\//i.test(u)) u = `https://${u}`;
|
|
9
|
+
return u;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isValidUrl(u) {
|
|
13
|
+
return /^https?:\/\/.+/i.test(u);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isValidToken(t) {
|
|
17
|
+
return typeof t === "string" && t.startsWith("bcf_") && t.length >= 12;
|
|
18
|
+
}
|