@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 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.3";
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
- Usage:
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
- Save your platform URL + dev token (~/.blocofy/credentials.json).
46
- Get a token from your admin panel: Settings → Theme CLI tokens.
47
- blocofy theme dev [dir] Local dev server — local theme + live data with instant preview.
48
- Edit a file and the browser reloads automatically. dir defaults to cwd.
49
- --port <n> (default 3030)
50
- blocofy theme pull [dir] Download the live theme to disk. dir defaults to cwd.
51
- blocofy theme push [dir] Write the local theme to the live site (create/update; no delete).
52
- --draft write to a draft theme instead (preview & publish from the admin panel)
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
- The CLI does not build assets — generate them with your own tools (npm/Vite/Tailwind);
57
- the platform serves plain Liquid + static assets.`);
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
- if (!url) url = (await rl.question("Platform/site URL (e.g. https://store.myblocofy.com): ")).trim();
69
- if (!token) token = (await rl.question("Dev token (bcf_…): ")).trim();
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(`Saved credentials: ${credentialsPath()}`);
87
- console.log(`Now run, in your theme directory: blocofy theme dev`);
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: http://localhost:${port}`);
159
- if (session) {
160
- console.log(` Preview: ${session.previewUrl}&hr=${port}`);
161
- console.log(` Editor: ${session.editorUrl}&hr=${port}`);
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;
@@ -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
- const watcher = startWatch(dir, async () => {
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
- /* senkron hatası reload'u engellemesin */
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
- const fire = () => {
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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Develop your Blocofy theme locally against live data with instant preview — login, theme dev, pull, push.",
5
5
  "type": "module",
6
6
  "license": "MIT",