@blocofy/cli 0.1.11 → 0.1.14

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
@@ -17,13 +17,20 @@ import { credentialsPath, loadCredentials, saveCredentials } from "../lib/creden
17
17
  import { startDevServer } from "../lib/dev-server.mjs";
18
18
  import { readLocalTemplates } from "../lib/local-theme.mjs";
19
19
  import { githubNote, statusLine, syncScopeNote } from "../lib/messages.mjs";
20
- import { fetchDevSession, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
20
+ import { fetchDevSession, fetchSiteStatus, fetchWhoami, publishInstance, pullTheme, pushTheme } from "../lib/theme-sync.mjs";
21
+ import { isAffirmative, livePushDecision } from "../lib/confirm.mjs";
21
22
  import { hyperlink, openUrl } from "../lib/term.mjs";
22
23
  import { isValidToken, isValidUrl, normalizeUrl } from "../lib/validate.mjs";
23
24
 
24
- const VERSION = "0.1.11";
25
+ const VERSION = "0.1.14";
25
26
  const args = process.argv.slice(2);
26
27
 
28
+ /** Human label for a resolved site: "Name (slug)" or just the slug. */
29
+ function siteLabel(site) {
30
+ if (!site) return "";
31
+ return site.name ? `${site.name} (${site.slug})` : site.slug;
32
+ }
33
+
27
34
  /** Parse `--key value` and `--flag` (boolean) arguments. */
28
35
  function parseFlags(rest) {
29
36
  const flags = {};
@@ -55,7 +62,8 @@ Usage
55
62
  blocofy theme dev [dir] [--port <n>] [--no-sync]
56
63
  Start a dev server and print 3 auto-reloading views — Local, live-domain
57
64
  Preview, and the theme Editor. Press l / p / e to open each, q to quit.
58
- Edit a file and save → every open view reloads. (dir defaults to cwd)
65
+ Edit a file and save → every open view reloads. Saves sync to a DRAFT theme
66
+ only (never the live site). (dir defaults to cwd)
59
67
  --port <n> local port (default 3030)
60
68
  --no-sync local preview only (skip draft sync + remote views)
61
69
 
@@ -63,9 +71,22 @@ Usage
63
71
  Download the live theme to disk. (dir defaults to cwd)
64
72
  --draft pull the draft theme (what 'theme dev' syncs into) instead of live
65
73
 
66
- blocofy theme push [dir] [--draft]
67
- Write the local theme to the site (create/update; no delete).
68
- --draft write to a draft theme — preview & publish it from the admin panel
74
+ blocofy theme push [dir] [--draft] [--yes]
75
+ Write the local theme to the LIVE site IMMEDIATELY (create/update; no delete,
76
+ no preview). Asks for confirmation first; non-interactive shells must pass --yes.
77
+ --draft write to a draft theme instead — preview & publish it from the
78
+ admin panel (recommended; never touches the live site)
79
+ --yes confirm the live push without prompting (for CI / agents)
80
+
81
+ blocofy theme publish [--instance <id>]
82
+ Publish a draft theme to the LIVE site. With no flag, publishes the draft that
83
+ 'theme dev' / 'theme push --draft' writes into. The server refuses to publish a
84
+ theme that has no pages (it would 404) — so publishing is always safe.
85
+ --instance <id> publish a specific theme instance
86
+
87
+ blocofy status
88
+ Show the live theme, page distribution per instance, drafts, and a health flag
89
+ (ok / live_instance_empty / pages_split). Run before/after publishing.
69
90
 
70
91
  blocofy pages pull [dir] / pages push [dir]
71
92
  pull: download published pages → pages/<slug>.json.
@@ -81,7 +102,8 @@ Usage
81
102
  Examples
82
103
  blocofy login --url https://store.myblocofy.com --token bcf_xxxxxxxx
83
104
  blocofy theme pull && blocofy theme dev
84
- blocofy theme push --draft
105
+ blocofy theme push --draft && blocofy theme publish
106
+ blocofy status
85
107
 
86
108
  Auth: ~/.blocofy/credentials.json (from \`login\`), or BLOCOFY_URL + BLOCOFY_TOKEN env vars.
87
109
  The CLI does not build assets — bring your own (npm/Vite/Tailwind); the platform serves
@@ -144,6 +166,16 @@ async function login(rest) {
144
166
 
145
167
  saveCredentials({ url, token });
146
168
  console.log(`✓ Saved credentials → ${credentialsPath()}`);
169
+
170
+ // Token'ın GERÇEK site'ını göster — site sunucuda TOKEN'dan çözülür, URL kozmetik.
171
+ // Yanlış tenant'ın token'ıyla login olduysan ("klarosa URL + ksc token") hemen
172
+ // görürsün. whoami yoksa/erişilemezse sessiz geç.
173
+ try {
174
+ const who = await fetchWhoami({ url, token });
175
+ if (who?.site) console.log(` Site: ${siteLabel(who.site)} — your theme commands target this tenant.`);
176
+ } catch {
177
+ /* best-effort */
178
+ }
147
179
  console.log(`Next: cd into your theme directory, then run blocofy theme dev`);
148
180
  }
149
181
 
@@ -177,6 +209,48 @@ async function themePush(rest) {
177
209
  }
178
210
  const creds = requireCreds();
179
211
  const draft = Boolean(flags.draft);
212
+
213
+ // Hedef tenant'ı çöz ve GÖSTER — site sunucuda TOKEN'dan çözülür (URL kozmetik),
214
+ // yanlış-tenant'a yazımı görünür kılar ("klarosa sandım, ksc'ye yazdım"). whoami
215
+ // yoksa/erişilemezse sessiz geç; etiketi confirmation mesajlarında da kullan.
216
+ let site = null;
217
+ try {
218
+ site = (await fetchWhoami({ url: creds.url, token: creds.token })).site;
219
+ } catch {
220
+ /* best-effort */
221
+ }
222
+ const target = site ? siteLabel(site) : creds.url;
223
+ if (site) {
224
+ console.log(`→ ${draft ? "Pushing to a draft" : "Pushing to the LIVE theme"} of ${target}`);
225
+ }
226
+
227
+ // Canlı push (flag'siz) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
228
+ // kazara canlıya basmasın diye açık onay şart (#431 L2).
229
+ const decision = livePushDecision({
230
+ draft,
231
+ yes: Boolean(flags.yes),
232
+ confirm: Boolean(flags.confirm),
233
+ isTTY: Boolean(process.stdin.isTTY),
234
+ });
235
+ if (decision.mustAbort) {
236
+ console.error(`⚠ 'theme push' writes to the LIVE theme of ${target} immediately (no preview).`);
237
+ console.error(` Non-interactive shell: pass --yes to confirm, or use --draft for a safe draft.`);
238
+ process.exit(1);
239
+ }
240
+ if (decision.needsPrompt) {
241
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
242
+ let answer;
243
+ try {
244
+ answer = await rl.question(`⚠ Push to the LIVE theme of ${target}? Immediate, no preview. [y/N] `);
245
+ } finally {
246
+ rl.close();
247
+ }
248
+ if (!isAffirmative(answer)) {
249
+ console.error("Aborted. Use `--draft` to push to a draft, or `--yes` to confirm the live push.");
250
+ process.exit(1);
251
+ }
252
+ }
253
+
180
254
  const result = await pushTheme({ dir, url: creds.url, token: creds.token, draft });
181
255
  if (result.draft) {
182
256
  console.log(`Pushed to draft theme #${result.instanceId} (${result.created} created, ${result.updated} updated).`);
@@ -250,7 +324,9 @@ async function themeDev(rest) {
250
324
  const previewUrl = session ? `${session.previewUrl}&hr=${port}` : null;
251
325
  const editorUrl = session ? `${session.editorUrl}&hr=${port}` : null;
252
326
 
253
- console.log(`\nblocofy theme dev — ${creds.url} (token ${creds.token.slice(0, 8)}…)\n`);
327
+ // Çözülen site'ı göster (session TOKEN'dan çözer) → hangi tenant'ı düzenlediğin belli olsun.
328
+ const siteLine = session?.site ? `${siteLabel(session.site)} · ` : "";
329
+ console.log(`\nblocofy theme dev — ${siteLine}${creds.url} (token ${creds.token.slice(0, 8)}…)\n`);
254
330
  console.log(` (l) Local ${hyperlink(localUrl)}`);
255
331
  if (previewUrl) console.log(` (p) Preview ${hyperlink(previewUrl)}`);
256
332
  if (editorUrl) console.log(` (e) Editor ${hyperlink(editorUrl)}`);
@@ -357,6 +433,45 @@ async function themeDev(rest) {
357
433
  }
358
434
  }
359
435
 
436
+ async function themePublish(rest) {
437
+ const flags = parseFlags(rest);
438
+ const creds = requireCreds();
439
+ let instanceId = Number(flags.instance);
440
+ if (!Number.isInteger(instanceId) || instanceId <= 0) {
441
+ // Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla.
442
+ const session = await fetchDevSession({ url: creds.url, token: creds.token });
443
+ instanceId = session.draftInstanceId;
444
+ }
445
+ const result = await publishInstance({ url: creds.url, token: creds.token, instanceId });
446
+ console.log(
447
+ `✓ Theme #${result.published} is now LIVE${result.cloned ? " (pages cloned from the previous live theme)" : ""}.`,
448
+ );
449
+ }
450
+
451
+ async function status() {
452
+ const creds = requireCreds();
453
+ const s = await fetchSiteStatus({ url: creds.url, token: creds.token });
454
+ const live = s.live_theme_instance;
455
+ console.log(`\nSite: ${s.site.slug} (id ${s.site.id})`);
456
+ console.log(
457
+ live
458
+ ? `Live theme: #${live.id}${live.name ? ` ${live.name}` : ""} — ${live.template_count} files, ${s.pages_on_live} pages`
459
+ : `Live theme: none`,
460
+ );
461
+ console.log(`Health: ${s.health}`);
462
+ if (s.health === "live_instance_empty") {
463
+ console.error(
464
+ ` ⚠ The live theme has no pages — the site will 404. Publish a theme with content: blocofy theme publish`,
465
+ );
466
+ } else if (s.health === "pages_split") {
467
+ console.warn(` ⚠ ${s.orphaned_pages} page(s) live on a non-live instance (orphan).`);
468
+ }
469
+ if (Array.isArray(s.drafts) && s.drafts.length > 0) {
470
+ console.log(`Drafts: ${s.drafts.map((d) => `#${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
471
+ }
472
+ console.log("");
473
+ }
474
+
360
475
  const [first, ...rest] = args;
361
476
 
362
477
  if (first === "--version" || first === "-v") {
@@ -383,6 +498,16 @@ if (first === "--version" || first === "-v") {
383
498
  console.error(error?.message ?? error);
384
499
  process.exit(1);
385
500
  });
501
+ } else if (first === "theme" && rest[0] === "publish") {
502
+ themePublish(rest.slice(1)).catch((error) => {
503
+ console.error(error?.message ?? error);
504
+ process.exit(1);
505
+ });
506
+ } else if (first === "status") {
507
+ status().catch((error) => {
508
+ console.error(error?.message ?? error);
509
+ process.exit(1);
510
+ });
386
511
  } else if (first === "pages" && rest[0] === "pull") {
387
512
  contentPull("pages", rest.slice(1)).catch((error) => {
388
513
  console.error(error?.message ?? error);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canlı `theme push` onay kararı (#431 L2). `theme push` (flag'siz) site'in CANLI
3
+ * temasına ANINDA yazar (önizleme yok) — bir agent/CI kazara canlı temayı bozabilir.
4
+ * Bu yüzden canlı push açık onay ister:
5
+ * - `--draft` → onay gerekmez (zaten canlıya yazmaz, taslağa yazar).
6
+ * - `--yes` / `--confirm` → açık onay (CI/agent için).
7
+ * - interaktif TTY → y/N sorulur.
8
+ * - non-TTY (script/agent) + onay yok → ABORT (sessizce canlıya basılmaz).
9
+ *
10
+ * Saf karar (yan etkisiz, test edilebilir): { autoApproved, needsPrompt, mustAbort }.
11
+ */
12
+ export function livePushDecision({ draft, yes, confirm, isTTY }) {
13
+ if (draft || yes || confirm) return { autoApproved: true, needsPrompt: false, mustAbort: false };
14
+ if (!isTTY) return { autoApproved: false, needsPrompt: false, mustAbort: true };
15
+ return { autoApproved: false, needsPrompt: true, mustAbort: false };
16
+ }
17
+
18
+ /** y/N yanıtını yorumla (yes/y → onay). */
19
+ export function isAffirmative(answer) {
20
+ const a = String(answer ?? "").trim().toLowerCase();
21
+ return a === "y" || a === "yes";
22
+ }
@@ -40,6 +40,21 @@ export async function pullTheme({ dir, url, token, draft = false }) {
40
40
  return { count };
41
41
  }
42
42
 
43
+ /**
44
+ * Token'ın GERÇEK site'ını çözer (`GET /api/dev/whoami`). Site sunucuda TOKEN'dan
45
+ * çözülür — login URL'i kozmetik. CLI bunu `login`'de (doğrula+göster) ve `push`
46
+ * öncesi (hedef tenant'ı yaz) çağırır; yanlış-tenant'a yazımı görünür kılar.
47
+ * `{ site: { id, slug, name }, liveThemeId }`.
48
+ */
49
+ export async function fetchWhoami({ url, token }) {
50
+ const base = url.replace(/\/+$/, "");
51
+ const res = await fetch(`${base}/api/dev/whoami`, {
52
+ headers: { authorization: `Bearer ${token}` },
53
+ });
54
+ if (!res.ok) throw new Error(await errorText(res));
55
+ return res.json();
56
+ }
57
+
43
58
  /**
44
59
  * Dev session bilgisi (#119 `theme dev`): platform draft instance'ı hazırlar ve
45
60
  * 3 görünümün URL'lerini döner — `{ draftInstanceId, previewUrl, editorUrl, site }`.
@@ -69,3 +84,30 @@ export async function pushTheme({ dir, url, token, draft = false }) {
69
84
  if (!res.ok) throw new Error(await errorText(res));
70
85
  return res.json();
71
86
  }
87
+
88
+ /**
89
+ * Bir taslak tema instance'ını CANLIYA al (`POST /api/dev/publish`). Sunucu guard'ı
90
+ * içi-sayfasız bir instance'ı reddeder ya da canlının sayfalarını klonlar (#431) —
91
+ * yayın sonrası site asla 404'e düşmez. `{ ok, published, cloned }` döner.
92
+ */
93
+ export async function publishInstance({ url, token, instanceId }) {
94
+ const base = url.replace(/\/+$/, "");
95
+ const res = await fetch(`${base}/api/dev/publish`, {
96
+ method: "POST",
97
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
98
+ body: JSON.stringify({ instanceId }),
99
+ });
100
+ if (!res.ok) throw new Error(await errorText(res));
101
+ return res.json();
102
+ }
103
+
104
+ /**
105
+ * Site sağlık/durum özeti (`GET /api/dev/site`) — `blocofy status`. Canlı tema instance'ı,
106
+ * instance-başına sayfa dağılımı, taslaklar ve health döner.
107
+ */
108
+ export async function fetchSiteStatus({ url, token }) {
109
+ const base = url.replace(/\/+$/, "");
110
+ const res = await fetch(`${base}/api/dev/site`, { headers: { authorization: `Bearer ${token}` } });
111
+ if (!res.ok) throw new Error(await errorText(res));
112
+ return res.json();
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
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",