@blocofy/cli 0.1.2 → 0.1.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 +10 -4
- package/bin/blocofy.mjs +136 -37
- package/lib/dev-server.mjs +22 -2
- package/lib/term.mjs +36 -0
- package/lib/theme-sync.mjs +13 -0
- package/lib/validate.mjs +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ npx @blocofy/cli theme dev # http://localhost:3030 — local theme + live
|
|
|
15
15
|
```bash
|
|
16
16
|
blocofy login [--url <url>] [--token <bcf_…>]
|
|
17
17
|
# Save your platform URL + dev token (~/.blocofy/credentials.json, 0600).
|
|
18
|
-
blocofy theme dev [dir] # Local dev server
|
|
19
|
-
#
|
|
18
|
+
blocofy theme dev [dir] # Local dev server. Prints 3 views — local preview, a live-domain
|
|
19
|
+
# preview link, and the theme editor — all auto-reloading on save.
|
|
20
|
+
# --port <n> (default 3030), --no-sync (local preview only)
|
|
20
21
|
blocofy theme pull [dir] # Download the live theme to disk.
|
|
21
22
|
blocofy theme push [dir] # Write the local theme to the live site (create/update; no delete).
|
|
22
23
|
blocofy --version
|
|
@@ -28,8 +29,13 @@ blocofy --help
|
|
|
28
29
|
`theme dev` starts a local HTTP server. For each page request it reads your local theme
|
|
29
30
|
files and sends them to the platform's dev-render endpoint (`/api/dev/render`). The platform
|
|
30
31
|
renders them with the site's **live data** and returns HTML — so the CLI ships no rendering
|
|
31
|
-
engine and you see exactly the production output.
|
|
32
|
-
|
|
32
|
+
engine and you see exactly the production output.
|
|
33
|
+
|
|
34
|
+
It also continuously syncs your local files to a **draft theme** so you can view the same work
|
|
35
|
+
three ways — the local preview, a shareable live-domain preview link, and the admin theme
|
|
36
|
+
editor — without affecting your published theme. Save a file and **every open view reloads**
|
|
37
|
+
(the platform-rendered pages connect back to the local dev server's reload channel). Publish
|
|
38
|
+
the draft from the theme editor when you're ready.
|
|
33
39
|
|
|
34
40
|
Credentials come from `~/.blocofy/credentials.json` (written by `blocofy login`) or the
|
|
35
41
|
`BLOCOFY_URL` + `BLOCOFY_TOKEN` environment variables (for CI/automation).
|
package/bin/blocofy.mjs
CHANGED
|
@@ -14,9 +14,11 @@ 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 { pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
17
|
+
import { fetchDevSession, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
|
|
18
|
+
import { hyperlink, openUrl } from "../lib/term.mjs";
|
|
19
|
+
import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
|
|
18
20
|
|
|
19
|
-
const VERSION = "0.1.
|
|
21
|
+
const VERSION = "0.1.4";
|
|
20
22
|
const args = process.argv.slice(2);
|
|
21
23
|
|
|
22
24
|
/** Parse `--key value` and `--flag` (boolean) arguments. */
|
|
@@ -40,51 +42,97 @@ function parseFlags(rest) {
|
|
|
40
42
|
function printHelp() {
|
|
41
43
|
console.log(`blocofy — Blocofy theme development CLI (v${VERSION})
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
Develop your theme locally against live data, preview it three ways, and publish.
|
|
46
|
+
|
|
47
|
+
Usage
|
|
44
48
|
blocofy login [--url <url>] [--token <bcf_…>]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
Save your platform URL + dev token to ~/.blocofy/credentials.json.
|
|
50
|
+
Get a token from the admin panel → Settings → Theme CLI tokens.
|
|
51
|
+
|
|
52
|
+
blocofy theme dev [dir] [--port <n>] [--no-sync]
|
|
53
|
+
Start a dev server and print 3 auto-reloading views — Local, live-domain
|
|
54
|
+
Preview, and the theme Editor. Press l / p / e to open each, q to quit.
|
|
55
|
+
Edit a file and save → every open view reloads. (dir defaults to cwd)
|
|
56
|
+
--port <n> local port (default 3030)
|
|
57
|
+
--no-sync local preview only (skip draft sync + remote views)
|
|
58
|
+
|
|
59
|
+
blocofy theme pull [dir]
|
|
60
|
+
Download the live theme to disk. (dir defaults to cwd)
|
|
61
|
+
|
|
62
|
+
blocofy theme push [dir] [--draft]
|
|
63
|
+
Write the local theme to the site (create/update; no delete).
|
|
64
|
+
--draft write to a draft theme — preview & publish it from the admin panel
|
|
65
|
+
|
|
53
66
|
blocofy --version
|
|
54
67
|
blocofy --help
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
Examples
|
|
70
|
+
blocofy login --url https://store.myblocofy.com --token bcf_xxxxxxxx
|
|
71
|
+
blocofy theme pull && blocofy theme dev
|
|
72
|
+
blocofy theme push --draft
|
|
73
|
+
|
|
74
|
+
Auth: ~/.blocofy/credentials.json (from \`login\`), or BLOCOFY_URL + BLOCOFY_TOKEN env vars.
|
|
75
|
+
The CLI does not build assets — bring your own (npm/Vite/Tailwind); the platform serves
|
|
76
|
+
plain Liquid + static assets.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Geçerli yanıt alana kadar sor (max `tries`), HER yanıtı anında doğrula. */
|
|
80
|
+
async function promptValid(rl, question, normalize, valid, hint, tries = 3) {
|
|
81
|
+
for (let i = 0; i < tries; i++) {
|
|
82
|
+
const answer = normalize(await rl.question(question));
|
|
83
|
+
if (valid(answer)) return answer;
|
|
84
|
+
console.error(` ✗ ${hint}`);
|
|
85
|
+
}
|
|
86
|
+
console.error("Too many invalid attempts.");
|
|
87
|
+
process.exit(1);
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
async function login(rest) {
|
|
61
91
|
const flags = parseFlags(rest);
|
|
62
|
-
let url = typeof flags.url === "string" ? flags.url : "";
|
|
63
|
-
let token = typeof flags.token === "string" ? flags.token : "";
|
|
92
|
+
let url = typeof flags.url === "string" ? normalizeUrl(flags.url) : "";
|
|
93
|
+
let token = typeof flags.token === "string" ? flags.token.trim() : "";
|
|
94
|
+
|
|
95
|
+
// Flag ile verildiyse anında doğrula (prompt'a düşmeden).
|
|
96
|
+
if (url && !isValidUrl(url)) {
|
|
97
|
+
console.error("Invalid --url — must be a valid http(s):// URL.");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
if (token && !isValidToken(token)) {
|
|
101
|
+
console.error("Invalid --token — must start with bcf_.");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
64
104
|
|
|
65
105
|
if (!url || !token) {
|
|
66
106
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
67
107
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
108
|
+
// URL'i token'dan ÖNCE iste ve ANINDA doğrula (şema yoksa https:// eklenir);
|
|
109
|
+
// geçersizse aynı anda tekrar sorar — token'a geçip sonra hata vermez.
|
|
110
|
+
if (!url) {
|
|
111
|
+
url = await promptValid(
|
|
112
|
+
rl,
|
|
113
|
+
"Platform/site URL (e.g. https://store.myblocofy.com): ",
|
|
114
|
+
normalizeUrl,
|
|
115
|
+
isValidUrl,
|
|
116
|
+
"Enter a valid URL, e.g. https://store.myblocofy.com",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!token) {
|
|
120
|
+
token = await promptValid(
|
|
121
|
+
rl,
|
|
122
|
+
"Dev token (bcf_…): ",
|
|
123
|
+
(s) => s.trim(),
|
|
124
|
+
isValidToken,
|
|
125
|
+
"Token must start with bcf_ — get one from the admin panel: Settings → Theme CLI tokens.",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
70
128
|
} finally {
|
|
71
129
|
rl.close();
|
|
72
130
|
}
|
|
73
131
|
}
|
|
74
132
|
|
|
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
133
|
saveCredentials({ url, token });
|
|
86
|
-
console.log(
|
|
87
|
-
console.log(`
|
|
134
|
+
console.log(`✓ Saved credentials → ${credentialsPath()}`);
|
|
135
|
+
console.log(`Next: cd into your theme directory, then run blocofy theme dev`);
|
|
88
136
|
}
|
|
89
137
|
|
|
90
138
|
/** Resolve credentials (URL + token) or exit with guidance. */
|
|
@@ -127,7 +175,7 @@ async function themePush(rest) {
|
|
|
127
175
|
}
|
|
128
176
|
}
|
|
129
177
|
|
|
130
|
-
function themeDev(rest) {
|
|
178
|
+
async function themeDev(rest) {
|
|
131
179
|
const flags = parseFlags(rest);
|
|
132
180
|
const positional = rest.filter((a) => !a.startsWith("--"));
|
|
133
181
|
const themeDir = resolve(positional[0] ?? process.cwd());
|
|
@@ -140,12 +188,33 @@ function themeDev(rest) {
|
|
|
140
188
|
const creds = requireCreds();
|
|
141
189
|
const port = Number(flags.port) || 3030;
|
|
142
190
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
191
|
+
// Dev session: live-domain preview + theme editor URLs (+ a draft to sync into).
|
|
192
|
+
// Graceful: if the platform can't provide one, fall back to local-only preview.
|
|
193
|
+
let session = null;
|
|
194
|
+
if (!flags["no-sync"]) {
|
|
195
|
+
try {
|
|
196
|
+
session = await fetchDevSession({ url: creds.url, token: creds.token });
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.warn(
|
|
199
|
+
`Warning: dev session unavailable (${error?.message ?? error}). ` +
|
|
200
|
+
`Local preview only — live-domain/editor views + draft sync disabled.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const localUrl = `http://localhost:${port}`;
|
|
206
|
+
const previewUrl = session ? `${session.previewUrl}&hr=${port}` : null;
|
|
207
|
+
const editorUrl = session ? `${session.editorUrl}&hr=${port}` : null;
|
|
208
|
+
|
|
209
|
+
console.log(`\nblocofy theme dev — ${creds.url} (token ${creds.token.slice(0, 8)}…)\n`);
|
|
210
|
+
console.log(` (l) Local ${hyperlink(localUrl)}`);
|
|
211
|
+
if (previewUrl) console.log(` (p) Preview ${hyperlink(previewUrl)}`);
|
|
212
|
+
if (editorUrl) console.log(` (e) Editor ${hyperlink(editorUrl)}`);
|
|
213
|
+
const keys = ["l local", previewUrl && "p preview", editorUrl && "e editor", "q quit"]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join(" ");
|
|
216
|
+
console.log(`\n Press: ${keys}`);
|
|
217
|
+
console.log(` Edit a theme file and save — every open view reloads automatically.\n`);
|
|
149
218
|
|
|
150
219
|
if (flags.dry) {
|
|
151
220
|
console.log("(--dry: server not started)");
|
|
@@ -157,6 +226,7 @@ function themeDev(rest) {
|
|
|
157
226
|
url: creds.url,
|
|
158
227
|
token: creds.token,
|
|
159
228
|
port,
|
|
229
|
+
syncDraft: Boolean(session),
|
|
160
230
|
onError: (err) => {
|
|
161
231
|
if (err && err.code === "EADDRINUSE") {
|
|
162
232
|
console.error(`Port ${port} is in use. Try a different port: blocofy theme dev --port <n>`);
|
|
@@ -167,11 +237,37 @@ function themeDev(rest) {
|
|
|
167
237
|
},
|
|
168
238
|
});
|
|
169
239
|
const shutdown = () => {
|
|
240
|
+
if (process.stdin.isTTY) {
|
|
241
|
+
try {
|
|
242
|
+
process.stdin.setRawMode(false);
|
|
243
|
+
} catch {
|
|
244
|
+
/* yoksay */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
170
247
|
handle.close();
|
|
171
248
|
process.exit(0);
|
|
172
249
|
};
|
|
173
250
|
process.on("SIGINT", shutdown);
|
|
174
251
|
process.on("SIGTERM", shutdown);
|
|
252
|
+
|
|
253
|
+
// Klavye kısayolları (TTY): l/p/e ilgili görünümü tarayıcıda açar, q/Ctrl-C çıkar.
|
|
254
|
+
// Raw mode'da SIGINT gelmez → Ctrl-C'yi () elle yakala.
|
|
255
|
+
if (process.stdin.isTTY) {
|
|
256
|
+
try {
|
|
257
|
+
process.stdin.setRawMode(true);
|
|
258
|
+
} catch {
|
|
259
|
+
return; // raw mode yoksa kısayolsuz devam (sunucu çalışmaya devam eder)
|
|
260
|
+
}
|
|
261
|
+
process.stdin.resume();
|
|
262
|
+
process.stdin.setEncoding("utf8");
|
|
263
|
+
process.stdin.on("data", (key) => {
|
|
264
|
+
const k = key.toLowerCase();
|
|
265
|
+
if (key === "" || k === "q") shutdown();
|
|
266
|
+
else if (k === "l") openUrl(localUrl);
|
|
267
|
+
else if (k === "p" && previewUrl) openUrl(previewUrl);
|
|
268
|
+
else if (k === "e" && editorUrl) openUrl(editorUrl);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
175
271
|
}
|
|
176
272
|
|
|
177
273
|
const [first, ...rest] = args;
|
|
@@ -186,7 +282,10 @@ if (first === "--version" || first === "-v") {
|
|
|
186
282
|
process.exit(1);
|
|
187
283
|
});
|
|
188
284
|
} else if (first === "theme" && rest[0] === "dev") {
|
|
189
|
-
themeDev(rest.slice(1))
|
|
285
|
+
themeDev(rest.slice(1)).catch((error) => {
|
|
286
|
+
console.error(error?.message ?? error);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
});
|
|
190
289
|
} else if (first === "theme" && rest[0] === "pull") {
|
|
191
290
|
themePull(rest.slice(1)).catch((error) => {
|
|
192
291
|
console.error(error?.message ?? error);
|
package/lib/dev-server.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
injectLivereload,
|
|
9
9
|
readLocalTemplates,
|
|
10
10
|
} from "./local-theme.mjs";
|
|
11
|
+
import { pushTheme } from "./theme-sync.mjs";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Standalone dev server. For each page request it reads the local theme files and
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
* separate request); direct `/asset/*` references are served from disk. Images/JS
|
|
19
20
|
* are remote (Directus/CDN), so no local serving is needed for them.
|
|
20
21
|
*/
|
|
21
|
-
export function startDevServer({ dir, url, token, port, onError }) {
|
|
22
|
+
export function startDevServer({ dir, url, token, port, syncDraft = false, onError }) {
|
|
22
23
|
const base = url.replace(/\/+$/, "");
|
|
23
24
|
const clients = new Set();
|
|
24
25
|
|
|
@@ -56,6 +57,8 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
56
57
|
"content-type": "text/event-stream",
|
|
57
58
|
"cache-control": "no-cache, no-transform",
|
|
58
59
|
connection: "keep-alive",
|
|
60
|
+
// Cross-origin iframe'ler (canlı-domain önizleme + editör) subscribe edebilsin.
|
|
61
|
+
"access-control-allow-origin": "*",
|
|
59
62
|
});
|
|
60
63
|
res.write("retry: 1000\n\n");
|
|
61
64
|
clients.add(res);
|
|
@@ -83,10 +86,27 @@ export function startDevServer({ dir, url, token, port, onError }) {
|
|
|
83
86
|
|
|
84
87
|
if (onError) server.on("error", onError);
|
|
85
88
|
|
|
86
|
-
const
|
|
89
|
+
const broadcast = () => {
|
|
87
90
|
for (const client of clients) client.write("data: reload\n\n");
|
|
91
|
+
};
|
|
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 () => {
|
|
95
|
+
if (syncDraft) {
|
|
96
|
+
try {
|
|
97
|
+
await pushTheme({ dir, url, token, draft: true });
|
|
98
|
+
} catch {
|
|
99
|
+
/* senkron hatası reload'u engellemesin */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
broadcast();
|
|
88
103
|
});
|
|
89
104
|
|
|
105
|
+
// Açılışta taslağı bir kez doldur → uzak görünümler hemen güncel local'i gösterir.
|
|
106
|
+
if (syncDraft) {
|
|
107
|
+
void pushTheme({ dir, url, token, draft: true }).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
server.listen(port);
|
|
91
111
|
|
|
92
112
|
return {
|
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/theme-sync.mjs
CHANGED
|
@@ -34,6 +34,19 @@ export async function pullTheme({ dir, url, token }) {
|
|
|
34
34
|
return { count };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Dev session bilgisi (#119 `theme dev`): platform draft instance'ı hazırlar ve
|
|
39
|
+
* 3 görünümün URL'lerini döner — `{ draftInstanceId, previewUrl, editorUrl, site }`.
|
|
40
|
+
*/
|
|
41
|
+
export async function fetchDevSession({ url, token }) {
|
|
42
|
+
const base = url.replace(/\/+$/, "");
|
|
43
|
+
const res = await fetch(`${base}/api/dev/session`, {
|
|
44
|
+
headers: { authorization: `Bearer ${token}` },
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) throw new Error(await errorText(res));
|
|
47
|
+
return res.json();
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
/**
|
|
38
51
|
* Write the local theme to the site (create/update; no delete). With `draft`,
|
|
39
52
|
* writes to a draft theme instance instead of the live theme — preview & publish
|
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
|
+
}
|