@blocofy/cli 0.1.14 → 0.2.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/bin/blocofy.mjs CHANGED
@@ -12,6 +12,7 @@ import { existsSync } from "node:fs";
12
12
  import { resolve } from "node:path";
13
13
  import { createInterface } from "node:readline/promises";
14
14
 
15
+ import { parseArgs } from "../lib/args.mjs";
15
16
  import { pullContent, pushContent } from "../lib/content-sync.mjs";
16
17
  import { credentialsPath, loadCredentials, saveCredentials } from "../lib/credentials.mjs";
17
18
  import { startDevServer } from "../lib/dev-server.mjs";
@@ -31,24 +32,6 @@ function siteLabel(site) {
31
32
  return site.name ? `${site.name} (${site.slug})` : site.slug;
32
33
  }
33
34
 
34
- /** Parse `--key value` and `--flag` (boolean) arguments. */
35
- function parseFlags(rest) {
36
- const flags = {};
37
- for (let i = 0; i < rest.length; i++) {
38
- const arg = rest[i];
39
- if (!arg.startsWith("--")) continue;
40
- const key = arg.slice(2);
41
- const next = rest[i + 1];
42
- if (next && !next.startsWith("--")) {
43
- flags[key] = next;
44
- i++;
45
- } else {
46
- flags[key] = true;
47
- }
48
- }
49
- return flags;
50
- }
51
-
52
35
  function printHelp() {
53
36
  console.log(`blocofy — Blocofy theme development CLI (v${VERSION})
54
37
 
@@ -70,6 +53,8 @@ Usage
70
53
  blocofy theme pull [dir] [--draft]
71
54
  Download the live theme to disk. (dir defaults to cwd)
72
55
  --draft pull the draft theme (what 'theme dev' syncs into) instead of live
56
+ --instance <handle> pull a specific theme by its handle (from the admin
57
+ panel theme card, or \`blocofy status\`)
73
58
 
74
59
  blocofy theme push [dir] [--draft] [--yes]
75
60
  Write the local theme to the LIVE site IMMEDIATELY (create/update; no delete,
@@ -77,12 +62,14 @@ Usage
77
62
  --draft write to a draft theme instead — preview & publish it from the
78
63
  admin panel (recommended; never touches the live site)
79
64
  --yes confirm the live push without prompting (for CI / agents)
65
+ --instance <handle> push to a specific theme by its handle (safe targeted
66
+ write — no live-confirmation prompt)
80
67
 
81
- blocofy theme publish [--instance <id>]
68
+ blocofy theme publish [--instance <handle>]
82
69
  Publish a draft theme to the LIVE site. With no flag, publishes the draft that
83
70
  'theme dev' / 'theme push --draft' writes into. The server refuses to publish a
84
71
  theme that has no pages (it would 404) — so publishing is always safe.
85
- --instance <id> publish a specific theme instance
72
+ --instance <handle> publish a specific theme (handle from the panel / status)
86
73
 
87
74
  blocofy status
88
75
  Show the live theme, page distribution per instance, drafts, and a health flag
@@ -122,7 +109,7 @@ async function promptValid(rl, question, normalize, valid, hint, tries = 3) {
122
109
  }
123
110
 
124
111
  async function login(rest) {
125
- const flags = parseFlags(rest);
112
+ const { flags } = parseArgs(rest);
126
113
  let url = typeof flags.url === "string" ? normalizeUrl(flags.url) : "";
127
114
  let token = typeof flags.token === "string" ? flags.token.trim() : "";
128
115
 
@@ -190,25 +177,26 @@ function requireCreds() {
190
177
  }
191
178
 
192
179
  async function themePull(rest) {
193
- const flags = parseFlags(rest);
194
- const positional = rest.filter((a) => !a.startsWith("--"));
195
- const dir = resolve(positional[0] ?? process.cwd());
180
+ const { flags, positionals } = parseArgs(rest);
181
+ const dir = resolve(positionals[0] ?? process.cwd());
196
182
  const creds = requireCreds();
197
183
  const draft = Boolean(flags.draft);
198
- const { count } = await pullTheme({ dir, url: creds.url, token: creds.token, draft });
199
- console.log(`Downloaded ${count} ${draft ? "draft" : "live"} theme files → ${dir}`);
184
+ const instance = typeof flags.instance === "string" ? flags.instance : null;
185
+ const { count } = await pullTheme({ dir, url: creds.url, token: creds.token, draft, instance });
186
+ const what = instance ? `instance ${instance}` : draft ? "draft" : "live";
187
+ console.log(`Downloaded ${count} ${what} theme files → ${dir}`);
200
188
  }
201
189
 
202
190
  async function themePush(rest) {
203
- const flags = parseFlags(rest);
204
- const positional = rest.filter((a) => !a.startsWith("--"));
205
- const dir = resolve(positional[0] ?? process.cwd());
191
+ const { flags, positionals } = parseArgs(rest);
192
+ const dir = resolve(positionals[0] ?? process.cwd());
206
193
  if (!existsSync(dir)) {
207
194
  console.error(`Theme directory not found: ${dir}`);
208
195
  process.exit(1);
209
196
  }
210
197
  const creds = requireCreds();
211
198
  const draft = Boolean(flags.draft);
199
+ const instance = typeof flags.instance === "string" ? flags.instance : null;
212
200
 
213
201
  // Hedef tenant'ı çöz ve GÖSTER — site sunucuda TOKEN'dan çözülür (URL kozmetik),
214
202
  // yanlış-tenant'a yazımı görünür kılar ("klarosa sandım, ksc'ye yazdım"). whoami
@@ -227,7 +215,7 @@ async function themePush(rest) {
227
215
  // Canlı push (flag'siz) ANINDA canlı temayı değiştirir (önizleme yok). Agent/CI
228
216
  // kazara canlıya basmasın diye açık onay şart (#431 L2).
229
217
  const decision = livePushDecision({
230
- draft,
218
+ draft: draft || Boolean(instance),
231
219
  yes: Boolean(flags.yes),
232
220
  confirm: Boolean(flags.confirm),
233
221
  isTTY: Boolean(process.stdin.isTTY),
@@ -251,9 +239,9 @@ async function themePush(rest) {
251
239
  }
252
240
  }
253
241
 
254
- const result = await pushTheme({ dir, url: creds.url, token: creds.token, draft });
255
- if (result.draft) {
256
- console.log(`Pushed to draft theme #${result.instanceId} (${result.created} created, ${result.updated} updated).`);
242
+ const result = await pushTheme({ dir, url: creds.url, token: creds.token, draft, instance });
243
+ if (result.instanceId) {
244
+ console.log(`Pushed to theme ${result.instanceId} (${result.created} created, ${result.updated} updated).`);
257
245
  console.log(`Preview & publish it in the admin panel: Theme → Theme library → "Open in editor".`);
258
246
  } else {
259
247
  const extra = result.skippedDeletes
@@ -264,8 +252,8 @@ async function themePush(rest) {
264
252
  }
265
253
 
266
254
  async function contentPush(scope, rest) {
267
- const positional = rest.filter((a) => !a.startsWith("--"));
268
- const dir = resolve(positional[0] ?? process.cwd());
255
+ const { positionals } = parseArgs(rest);
256
+ const dir = resolve(positionals[0] ?? process.cwd());
269
257
  if (!existsSync(dir)) {
270
258
  console.error(`Directory not found: ${dir}`);
271
259
  process.exit(1);
@@ -286,17 +274,16 @@ async function contentPush(scope, rest) {
286
274
  }
287
275
 
288
276
  async function contentPull(scope, rest) {
289
- const positional = rest.filter((a) => !a.startsWith("--"));
290
- const dir = resolve(positional[0] ?? process.cwd());
277
+ const { positionals } = parseArgs(rest);
278
+ const dir = resolve(positionals[0] ?? process.cwd());
291
279
  const creds = requireCreds();
292
280
  const { count } = await pullContent({ dir, url: creds.url, token: creds.token, scope });
293
281
  console.log(`Downloaded ${count} ${scope === "settings" ? "settings" : "page"} file(s) → ${dir}`);
294
282
  }
295
283
 
296
284
  async function themeDev(rest) {
297
- const flags = parseFlags(rest);
298
- const positional = rest.filter((a) => !a.startsWith("--"));
299
- const themeDir = resolve(positional[0] ?? process.cwd());
285
+ const { flags, positionals } = parseArgs(rest);
286
+ const themeDir = resolve(positionals[0] ?? process.cwd());
300
287
 
301
288
  if (!existsSync(themeDir)) {
302
289
  console.error(`Theme directory not found: ${themeDir}`);
@@ -434,17 +421,17 @@ async function themeDev(rest) {
434
421
  }
435
422
 
436
423
  async function themePublish(rest) {
437
- const flags = parseFlags(rest);
424
+ const { flags } = parseArgs(rest);
438
425
  const creds = requireCreds();
439
- let instanceId = Number(flags.instance);
440
- if (!Number.isInteger(instanceId) || instanceId <= 0) {
426
+ let instance = typeof flags.instance === "string" ? flags.instance : null;
427
+ if (!instance) {
441
428
  // Belirtilmediyse: `theme dev` / `theme push --draft`'ın yazdığı taslağı yayınla.
442
429
  const session = await fetchDevSession({ url: creds.url, token: creds.token });
443
- instanceId = session.draftInstanceId;
430
+ instance = session.draftInstanceId;
444
431
  }
445
- const result = await publishInstance({ url: creds.url, token: creds.token, instanceId });
432
+ const result = await publishInstance({ url: creds.url, token: creds.token, instanceId: instance });
446
433
  console.log(
447
- `✓ Theme #${result.published} is now LIVE${result.cloned ? " (pages cloned from the previous live theme)" : ""}.`,
434
+ `✓ Theme ${result.published} is now LIVE${result.cloned ? " (pages cloned from the previous live theme)" : ""}.`,
448
435
  );
449
436
  }
450
437
 
@@ -452,10 +439,10 @@ async function status() {
452
439
  const creds = requireCreds();
453
440
  const s = await fetchSiteStatus({ url: creds.url, token: creds.token });
454
441
  const live = s.live_theme_instance;
455
- console.log(`\nSite: ${s.site.slug} (id ${s.site.id})`);
442
+ console.log(`\nSite: ${s.site.slug} · ${s.site.id}`);
456
443
  console.log(
457
444
  live
458
- ? `Live theme: #${live.id}${live.name ? ` ${live.name}` : ""} — ${live.template_count} files, ${s.pages_on_live} pages`
445
+ ? `Live theme: ${live.id}${live.name ? ` ${live.name}` : ""} — ${live.template_count} files, ${s.pages_on_live} pages`
459
446
  : `Live theme: none`,
460
447
  );
461
448
  console.log(`Health: ${s.health}`);
@@ -467,7 +454,7 @@ async function status() {
467
454
  console.warn(` ⚠ ${s.orphaned_pages} page(s) live on a non-live instance (orphan).`);
468
455
  }
469
456
  if (Array.isArray(s.drafts) && s.drafts.length > 0) {
470
- console.log(`Drafts: ${s.drafts.map((d) => `#${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
457
+ console.log(`Drafts: ${s.drafts.map((d) => `${d.id}${d.name ? ` ${d.name}` : ""}`).join(", ")}`);
471
458
  }
472
459
  console.log("");
473
460
  }
package/lib/args.mjs ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Birleşik arg tokenizer. Tek geçişte hem flag'leri hem positional'ları çıkarır —
3
+ * böylece bir flag DEĞERİ (örn. `--port 3035`'in `3035`'i) yanlışlıkla positional
4
+ * (tema dizini) sanılmaz. Boolean flag'ler sonraki token'ı YUTMAZ.
5
+ */
6
+
7
+ /** Sonraki token'ı değer olarak almayan flag'ler. */
8
+ const BOOLEAN_FLAGS = new Set(["draft", "yes", "confirm", "no-sync", "dry", "help", "version"]);
9
+
10
+ export function parseArgs(rest) {
11
+ const flags = {};
12
+ const positionals = [];
13
+ for (let i = 0; i < rest.length; i++) {
14
+ const arg = rest[i];
15
+ if (!arg.startsWith("--")) {
16
+ positionals.push(arg);
17
+ continue;
18
+ }
19
+ const key = arg.slice(2);
20
+ if (BOOLEAN_FLAGS.has(key)) {
21
+ flags[key] = true;
22
+ continue;
23
+ }
24
+ const next = rest[i + 1];
25
+ if (next !== undefined && !next.startsWith("--")) {
26
+ flags[key] = next;
27
+ i++;
28
+ } else {
29
+ flags[key] = true;
30
+ }
31
+ }
32
+ return { flags, positionals };
33
+ }
package/lib/messages.mjs CHANGED
@@ -26,8 +26,8 @@ export function syncScopeNote() {
26
26
  */
27
27
  export function statusLine(session) {
28
28
  if (!session) return null;
29
- const draft = session.draftInstanceId != null ? `#${session.draftInstanceId}` : "—";
30
- const live = session.liveThemeId != null ? `#${session.liveThemeId}` : "(none)";
29
+ const draft = session.draftInstanceId != null ? session.draftInstanceId : "—";
30
+ const live = session.liveThemeId != null ? session.liveThemeId : "(none)";
31
31
  return `Local files → Draft theme ${draft} · Live theme → ${live}`;
32
32
  }
33
33
 
@@ -23,9 +23,10 @@ async function errorText(res) {
23
23
  * With `draft`, pulls the "CLI Draft" instance (what `theme dev` syncs into)
24
24
  * instead of the live theme — symmetric with `push --draft`.
25
25
  */
26
- export async function pullTheme({ dir, url, token, draft = false }) {
26
+ export async function pullTheme({ dir, url, token, draft = false, instance = null }) {
27
27
  const base = url.replace(/\/+$/, "");
28
- const res = await fetch(`${base}/api/dev/theme${draft ? "?draft=1" : ""}`, {
28
+ const query = instance ? `?instance=${encodeURIComponent(instance)}` : draft ? "?draft=1" : "";
29
+ const res = await fetch(`${base}/api/dev/theme${query}`, {
29
30
  headers: { authorization: `Bearer ${token}` },
30
31
  });
31
32
  if (!res.ok) throw new Error(await errorText(res));
@@ -73,13 +74,14 @@ export async function fetchDevSession({ url, token }) {
73
74
  * writes to a draft theme instance instead of the live theme — preview & publish
74
75
  * it from the admin panel without affecting the live site.
75
76
  */
76
- export async function pushTheme({ dir, url, token, draft = false }) {
77
+ export async function pushTheme({ dir, url, token, draft = false, instance = null }) {
77
78
  const base = url.replace(/\/+$/, "");
78
79
  const files = readLocalTemplates(dir);
80
+ const payload = instance ? { files, instance } : { files, draft };
79
81
  const res = await fetch(`${base}/api/dev/theme`, {
80
82
  method: "POST",
81
83
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
82
- body: JSON.stringify({ files, draft }),
84
+ body: JSON.stringify(payload),
83
85
  });
84
86
  if (!res.ok) throw new Error(await errorText(res));
85
87
  return res.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocofy/cli",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
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",