@zalify/cli 0.10.0 → 0.12.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.
Files changed (2) hide show
  1. package/dist/cli.js +295 -144
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11860,7 +11860,7 @@ var POLL_INTERVAL_MS = 5000;
11860
11860
  var POLL_TIMEOUT_MS = 30 * 60 * 1000;
11861
11861
  var STREAM_BATCH = 20;
11862
11862
  function buildPrompt(manifest, entry) {
11863
- const style = entry.type === "model" ? manifest.modelStyle ?? manifest.style : manifest.style;
11863
+ const style = entry.type === "model" ? manifest.modelStyle ?? manifest.style : entry.type === "banner" ? manifest.bannerStyle ?? manifest.style : manifest.style;
11864
11864
  return `${style}
11865
11865
 
11866
11866
  ${entry.prompt}`;
@@ -11903,7 +11903,7 @@ async function submitJob(auth, manifest, entries) {
11903
11903
  workspaceId: auth.workspaceId,
11904
11904
  jobs: entries.map((e) => ({
11905
11905
  prompt: buildPrompt(manifest, e),
11906
- size: "1024x1024",
11906
+ size: e.size ?? "1024x1024",
11907
11907
  count: 1
11908
11908
  }))
11909
11909
  })
@@ -11970,7 +11970,7 @@ async function streamingGenerate(auth, manifest, missing, imagesDir, indexPath,
11970
11970
  jobs: batch.map((e) => ({
11971
11971
  prompt: buildPrompt(manifest, e),
11972
11972
  count: 1,
11973
- size: "1024x1024"
11973
+ size: e.size ?? "1024x1024"
11974
11974
  }))
11975
11975
  })
11976
11976
  });
@@ -12064,9 +12064,9 @@ async function imagesGenerate(storeDir) {
12064
12064
  import { readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
12065
12065
  import { basename as basename3, join as join6, resolve as resolve5 } from "node:path";
12066
12066
 
12067
- // src/storefront.ts
12068
- import { spawnSync as spawnSync3 } from "node:child_process";
12069
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
12067
+ // src/site.ts
12068
+ import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
12069
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
12070
12070
  import { basename as basename2, join as join5, resolve as resolve4 } from "node:path";
12071
12071
 
12072
12072
  // src/theme.ts
@@ -12493,11 +12493,11 @@ function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
12493
12493
  return depsChanged;
12494
12494
  }
12495
12495
 
12496
- // src/storefront.ts
12496
+ // src/site.ts
12497
12497
  function apiError2(status, json, appUrl) {
12498
12498
  if (status === 402) {
12499
12499
  return new Error(`${json.error ?? "Upgrade required"}
12500
- ` + ` Storefront hosting is a paid feature — upgrade the workspace in ${appUrl}`);
12500
+ ` + ` Site hosting is a paid feature — upgrade the workspace in ${appUrl}`);
12501
12501
  }
12502
12502
  if (json.code === "NO_CONNECTION") {
12503
12503
  return new Error(`${json.error}
@@ -12508,40 +12508,44 @@ function apiError2(status, json, appUrl) {
12508
12508
  Pick a different directory/slug name.`);
12509
12509
  }
12510
12510
  if (status === 404) {
12511
- return new Error("The storefronts API is not deployed yet (404). Update app.zalify.com first.");
12511
+ return new Error("The sites API is not deployed yet (404). Update app.zalify.com first.");
12512
12512
  }
12513
12513
  if (status === 503) {
12514
- return new Error(json.error ?? "Storefront provisioning is not configured on the server.");
12514
+ return new Error(json.error ?? "Site provisioning is not configured on the server.");
12515
12515
  }
12516
- return new Error(`storefronts API ${status}: ${JSON.stringify(json)}`);
12516
+ return new Error(`sites API ${status}: ${JSON.stringify(json)}`);
12517
12517
  }
12518
- async function post(auth, path9, body) {
12519
- const res = await fetch(`${auth.appUrl}${path9}`, {
12520
- method: "POST",
12521
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${auth.key}` },
12522
- body: JSON.stringify({ workspaceId: auth.workspaceId, ...body })
12523
- });
12518
+ async function request(auth, method, path9, body) {
12519
+ const attempt = async (p) => {
12520
+ const url = method === "GET" ? `${auth.appUrl}${p}${p.includes("?") ? "&" : "?"}workspaceId=${auth.workspaceId}` : `${auth.appUrl}${p}`;
12521
+ return fetch(url, {
12522
+ method,
12523
+ headers: {
12524
+ Authorization: `Bearer ${auth.key}`,
12525
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
12526
+ },
12527
+ ...method === "POST" ? { body: JSON.stringify({ workspaceId: auth.workspaceId, ...body }) } : {}
12528
+ });
12529
+ };
12530
+ let res = await attempt(path9);
12531
+ if (res.status === 404 && path9.startsWith("/api/sites")) {
12532
+ res = await attempt(path9.replace("/api/sites", "/api/storefronts"));
12533
+ }
12524
12534
  const json = await res.json().catch(() => ({}));
12525
12535
  if (!res.ok)
12526
12536
  throw apiError2(res.status, json, auth.appUrl);
12527
12537
  return json;
12528
12538
  }
12529
- async function get(auth, path9) {
12530
- const sep = path9.includes("?") ? "&" : "?";
12531
- const res = await fetch(`${auth.appUrl}${path9}${sep}workspaceId=${auth.workspaceId}`, {
12532
- headers: { Authorization: `Bearer ${auth.key}` }
12533
- });
12534
- const json = await res.json().catch(() => ({}));
12535
- if (!res.ok)
12536
- throw apiError2(res.status, json, auth.appUrl);
12537
- return json;
12539
+ async function listSites(auth) {
12540
+ const json = await request(auth, "GET", "/api/sites");
12541
+ return json.sites ?? json.storefronts ?? [];
12538
12542
  }
12539
12543
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
12540
12544
  async function pollUntilReady(auth, id) {
12541
12545
  let lastMessage = "";
12542
12546
  const deadline = Date.now() + 10 * 60000;
12543
12547
  while (Date.now() < deadline) {
12544
- const view = await get(auth, `/api/storefronts/${id}`);
12548
+ const view = await request(auth, "GET", `/api/sites/${id}`);
12545
12549
  const message = view.statusMessage ?? view.status;
12546
12550
  if (message !== lastMessage) {
12547
12551
  console.log(` … ${message}`);
@@ -12556,16 +12560,19 @@ async function pollUntilReady(auth, id) {
12556
12560
  }
12557
12561
  throw new Error("Provisioning timed out after 10 minutes — check the Temporal UI.");
12558
12562
  }
12559
- async function storefrontCreate(dir2, opts) {
12563
+ async function siteCreate(dir2, opts) {
12560
12564
  const auth = requireActive();
12561
- const slug = basename2(resolve4(dir2)).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "storefront";
12562
- console.log(`Provisioning storefront "${slug}" (workspace "${auth.workspaceName}")`);
12563
- const { storefrontId } = await post(auth, "/api/storefronts", {
12565
+ const slug = basename2(resolve4(dir2)).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "site";
12566
+ console.log(`Provisioning site "${slug}" (workspace "${auth.workspaceName}")`);
12567
+ const created = await request(auth, "POST", "/api/sites", {
12564
12568
  slug,
12565
12569
  theme: opts.theme ?? "nextjs",
12566
12570
  ...opts.to ? { themeVersion: opts.to } : {}
12567
12571
  });
12568
- const view = await pollUntilReady(auth, storefrontId);
12572
+ const siteId = created.siteId ?? created.storefrontId;
12573
+ if (!siteId)
12574
+ throw new Error("provisioning API returned no site id");
12575
+ const view = await pollUntilReady(auth, siteId);
12569
12576
  console.log(` ✓ provisioned: ${view.githubRepo} → https://${view.domain}`);
12570
12577
  await themeCreate(dir2, {
12571
12578
  template: view.theme,
@@ -12576,9 +12583,13 @@ async function storefrontCreate(dir2, opts) {
12576
12583
  install: opts.install,
12577
12584
  git: opts.git
12578
12585
  });
12586
+ writeFileSync6(join5(resolve4(dir2), ".zalify", "site.json"), JSON.stringify({ id: siteId, slug: view.slug, domain: view.domain, repo: view.githubRepo }, null, 2) + `
12587
+ `);
12579
12588
  if (opts.git !== false && view.githubRepo) {
12580
- const { token, repo } = await post(auth, `/api/storefronts/${storefrontId}/push-token`, {});
12589
+ const { token, repo } = await request(auth, "POST", `/api/sites/${siteId}/push-token`, {});
12581
12590
  const run = (args) => spawnSync3("git", args, { cwd: resolve4(dir2), stdio: "ignore" });
12591
+ run(["add", ".zalify/site.json"]);
12592
+ run(["commit", "-m", "Link site record", "--no-verify"]);
12582
12593
  run(["remote", "add", "origin", `https://github.com/${repo}.git`]);
12583
12594
  const push = spawnSync3("git", ["push", `https://x-access-token:${token}@github.com/${repo}.git`, "main"], { cwd: resolve4(dir2), stdio: "inherit" });
12584
12595
  if (push.status !== 0) {
@@ -12587,7 +12598,7 @@ async function storefrontCreate(dir2, opts) {
12587
12598
  run(["update-ref", "refs/remotes/origin/main", "HEAD"]);
12588
12599
  run(["config", "branch.main.remote", "origin"]);
12589
12600
  run(["config", "branch.main.merge", "refs/heads/main"]);
12590
- console.log(` ✓ pushed to github.com/${repo} — Vercel deploys on push`);
12601
+ console.log(` ✓ pushed to github.com/${repo} — deploys on push`);
12591
12602
  }
12592
12603
  if (view.domain) {
12593
12604
  console.log(` Waiting for https://${view.domain} …`);
@@ -12607,49 +12618,89 @@ Live: https://${view.domain}`);
12607
12618
  console.log(`
12608
12619
  Next steps:
12609
12620
  ` + ` - author your theme in ${dir2}/theme/ (see theme/README.md), commit + push
12610
- ` + ` - zalify shopify import stores/<slug> --prune # catalog (auto-flushes the site cache)
12611
- ` + ` - zalify theme status ${dir2} # drift check anytime`);
12621
+ ` + ` - zalify brand push <brand-dir> --prune # catalog (auto-refreshes this site)
12622
+ ` + ` - zalify site open ${slug} # see it live
12623
+ ` + ` - zalify theme status ${dir2} # drift check anytime`);
12624
+ }
12625
+ async function resolveSite(auth, slugOrDir) {
12626
+ const fromDir = (dir2) => {
12627
+ const link = join5(dir2, ".zalify", "site.json");
12628
+ if (existsSync5(link)) {
12629
+ const parsed = JSON.parse(readFileSync5(link, "utf8"));
12630
+ if (parsed.slug)
12631
+ return parsed.slug;
12632
+ }
12633
+ const storeJson = join5(dir2, "store.json");
12634
+ if (existsSync5(storeJson)) {
12635
+ const parsed = JSON.parse(readFileSync5(storeJson, "utf8"));
12636
+ if (parsed.slug)
12637
+ return parsed.slug;
12638
+ }
12639
+ return null;
12640
+ };
12641
+ let slug = null;
12642
+ if (slugOrDir && existsSync5(resolve4(slugOrDir)))
12643
+ slug = fromDir(resolve4(slugOrDir));
12644
+ if (!slug && slugOrDir)
12645
+ slug = slugOrDir;
12646
+ if (!slug)
12647
+ slug = fromDir(process.cwd());
12648
+ const sites = await listSites(auth);
12649
+ if (slug) {
12650
+ const match = sites.find((s) => s.slug === slug);
12651
+ if (match)
12652
+ return match;
12653
+ throw new Error(`No site "${slug}" in workspace "${auth.workspaceName}".
12654
+ ` + ` Sites here: ${sites.map((s) => s.slug).join(", ") || "(none)"}`);
12655
+ }
12656
+ if (sites.length === 1)
12657
+ return sites[0];
12658
+ throw new Error(sites.length === 0 ? `No sites in workspace "${auth.workspaceName}" — create one with \`zalify site create <dir>\`.` : `Multiple sites in this workspace — name one: ${sites.map((s) => s.slug).join(", ")}`);
12612
12659
  }
12613
- async function storefrontList() {
12660
+ async function siteList() {
12614
12661
  const auth = requireActive();
12615
- const { storefronts } = await get(auth, "/api/storefronts");
12616
- if (!storefronts.length) {
12617
- console.log(`No storefronts in workspace "${auth.workspaceName}".`);
12662
+ const sites = await listSites(auth);
12663
+ if (!sites.length) {
12664
+ console.log(`No sites in workspace "${auth.workspaceName}".`);
12618
12665
  return;
12619
12666
  }
12620
- for (const s of storefronts) {
12621
- const site = s.domain ? `https://${s.domain}` : "-";
12622
- console.log(`${s.status === "READY" ? "✓" : s.status === "FAILED" ? "✗" : "…"} ${s.slug} ${site} (${s.status.toLowerCase()}${s.githubRepo ? `, ${s.githubRepo}` : ""})`);
12667
+ for (const s of sites) {
12668
+ const url = s.domain ? `https://${s.domain}` : "-";
12669
+ console.log(`${s.status === "READY" ? "✓" : s.status === "FAILED" ? "✗" : "…"} ${s.slug} ${url} (${s.status.toLowerCase()}${s.githubRepo ? `, ${s.githubRepo}` : ""})`);
12623
12670
  }
12624
12671
  }
12625
- function resolveSlug(slugOrDir) {
12626
- const storeJson = join5(resolve4(slugOrDir), "store.json");
12627
- if (existsSync5(storeJson)) {
12628
- const parsed = JSON.parse(readFileSync5(storeJson, "utf8"));
12629
- if (parsed.slug)
12630
- return parsed.slug;
12631
- }
12632
- return basename2(slugOrDir);
12672
+ async function siteRefresh(slugOrDir) {
12673
+ const auth = requireActive();
12674
+ const site = await resolveSite(auth, slugOrDir);
12675
+ await request(auth, "POST", `/api/sites/${site.id}/revalidate`, {});
12676
+ console.log(`✓ refreshed https://${site.domain}`);
12633
12677
  }
12634
- async function storefrontRevalidate(slugOrDir) {
12678
+ async function siteOpen(slugOrDir) {
12635
12679
  const auth = requireActive();
12636
- const slug = resolveSlug(slugOrDir);
12637
- const { storefronts } = await get(auth, "/api/storefronts");
12638
- const match = storefronts.find((s) => s.slug === slug);
12639
- if (!match)
12640
- throw new Error(`No storefront "${slug}" in workspace "${auth.workspaceName}".`);
12641
- await post(auth, `/api/storefronts/${match.id}/revalidate`, {});
12642
- console.log(`✓ flushed cache on https://${match.domain}`);
12643
- }
12644
- async function maybeRevalidateStorefront(auth, storeDir) {
12680
+ const site = await resolveSite(auth, slugOrDir);
12681
+ if (!site.domain)
12682
+ throw new Error(`Site "${site.slug}" has no domain yet.`);
12683
+ const url = `https://${site.domain}`;
12684
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
12685
+ spawn3(opener, [url], { detached: true, stdio: "ignore" }).unref();
12686
+ console.log(url);
12687
+ }
12688
+ async function maybeRefreshSites(auth, brandDir, siteSlugOverride) {
12645
12689
  try {
12646
- const slug = resolveSlug(storeDir);
12647
- const { storefronts } = await get(auth, "/api/storefronts");
12648
- const match = storefronts.find((s) => s.slug === slug && s.status === "READY");
12649
- if (!match)
12690
+ let slug = siteSlugOverride ?? null;
12691
+ if (!slug) {
12692
+ const storeJson = join5(resolve4(brandDir), "store.json");
12693
+ if (existsSync5(storeJson)) {
12694
+ slug = JSON.parse(readFileSync5(storeJson, "utf8")).slug ?? null;
12695
+ }
12696
+ }
12697
+ if (!slug)
12650
12698
  return;
12651
- await post(auth, `/api/storefronts/${match.id}/revalidate`, {});
12652
- console.log(`✓ site cache flushed (https://${match.domain})`);
12699
+ const sites = (await listSites(auth)).filter((s) => s.slug === slug && s.status === "READY");
12700
+ for (const site of sites) {
12701
+ await request(auth, "POST", `/api/sites/${site.id}/revalidate`, {});
12702
+ console.log(`✓ site refreshed (https://${site.domain})`);
12703
+ }
12653
12704
  } catch {}
12654
12705
  }
12655
12706
 
@@ -12829,14 +12880,16 @@ async function shopifyImport(storeDir, options = {}) {
12829
12880
  console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
12830
12881
  }
12831
12882
  console.log("Done.");
12832
- await maybeRevalidateStorefront(auth, dir2);
12883
+ await maybeRefreshSites(auth, dir2, options.site);
12833
12884
  }
12834
- async function shopifyUploadImages(storeDir) {
12885
+ async function shopifyUploadImages(storeDir, options = {}) {
12835
12886
  const auth = requireActive();
12836
12887
  const dir2 = resolve5(storeDir);
12837
12888
  const manifest = readJson(join6(dir2, "images", "manifest.json"), "images/manifest.json");
12838
12889
  console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
12839
12890
  for (const entry of manifest.images) {
12891
+ if (entry.type === "banner" || !entry.handle)
12892
+ continue;
12840
12893
  const filePath = join6(dir2, "images", entry.file);
12841
12894
  if (!existsSync6(filePath)) {
12842
12895
  console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
@@ -12907,23 +12960,23 @@ async function shopifyUploadImages(storeDir) {
12907
12960
  console.log(` ✓ ${entry.file}`);
12908
12961
  }
12909
12962
  console.log("Done.");
12910
- await maybeRevalidateStorefront(auth, dir2);
12963
+ await maybeRefreshSites(auth, dir2, options.site);
12911
12964
  }
12912
12965
 
12913
- // src/store.ts
12914
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
12966
+ // src/brand.ts
12967
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "node:fs";
12915
12968
  import { basename as basename4, join as join7, resolve as resolve6 } from "node:path";
12916
- var AUTHORING_MD = `# Store authoring guide
12969
+ var AUTHORING_MD = `# Brand authoring guide
12917
12970
 
12918
12971
  The rules for filling in this folder. An author — human or AI agent —
12919
- should be able to produce a complete, import-ready store from this file
12920
- alone. Validate anytime with \`zalify store validate .\`.
12972
+ should be able to produce a complete, import-ready brand from this file
12973
+ alone. Validate anytime with \`zalify brand validate\` (defaults to cwd).
12921
12974
 
12922
12975
  ## Files
12923
12976
 
12924
12977
  | File | Role |
12925
12978
  | ---- | ---- |
12926
- | store.json | identity: name, slug, vertical, currency; storeDomain filled in once the Shopify dev store exists |
12979
+ | store.json | the Shopify store this brand connects to (name, slug, vertical, currency; storeDomain filled in once the store exists) |
12927
12980
  | brand.md | brand voice guide — tone, audience, naming, price band. Written FIRST; everything else follows it |
12928
12981
  | catalog.json | canonical catalog (products + collections) — the single source the pipeline imports |
12929
12982
  | images/manifest.json | one entry per shot + shared style/modelStyle art-direction prefixes |
@@ -12931,7 +12984,7 @@ alone. Validate anytime with \`zalify store validate .\`.
12931
12984
 
12932
12985
  ## catalog.json rules
12933
12986
 
12934
- - 10–14 products is the sweet spot for a demo store; every product needs
12987
+ - 10–14 products is the sweet spot for a demo brand; every product needs
12935
12988
  \`handle\` (unique, kebab-case, no SKU-speak), \`title\`, \`descriptionHtml\`
12936
12989
  (brand voice; simple HTML: p/ul/li/em only), \`type\`, \`tags\`, \`variants\`.
12937
12990
  - **Collections are smart collections driven by tags**: a product joins
@@ -12956,23 +13009,37 @@ alone. Validate anytime with \`zalify store validate .\`.
12956
13009
  - \`style\` prefixes packshot/detail prompts; \`modelStyle\` prefixes
12957
13010
  \`type: "model"\` prompts. Keep prompts concrete: materials, colors,
12958
13011
  camera angle, light.
13012
+ - **Banner / merchandising entries** (\`"type": "banner"\`, no \`handle\`)
13013
+ are theme imagery, not product shots: hero slides, editorial banners.
13014
+ They are generated and pushed to the asset library like any entry, but
13015
+ never attached to a product — theme templates reference them by their
13016
+ \`images/assets.json\` URL. Conventions:
13017
+ - Naming: \`hero-1.png\` + \`hero-1-mobile.png\`, \`hero-2.png\`…,
13018
+ \`editorial-1.png\`.
13019
+ - \`size\`: hero desktop + editorial \`"1536x1024"\`, hero mobile
13020
+ \`"1024x1536"\` (default is \`"1024x1024"\` — never right for banners).
13021
+ - \`bannerStyle\` prefixes banner prompts (falls back to \`style\`, whose
13022
+ "square 1:1" phrasing is wrong for banners — always set it).
13023
+ - NO text, lettering, or logos inside the image; the theme overlays
13024
+ copy. Leave calm negative space on one side for it.
12959
13025
  - **Prompt safety (gpt-image-2 rejects [sexual] otherwise)** — for
12960
13026
  human-model shots: say "woman"/"man", never "young …"; keep fitted
12961
13027
  garments layered or covered (blazer / cardigan / cover-up); crop at
12962
13028
  the chin or shoot from behind; prefer wider framing. Swimwear: show
12963
13029
  the garment via flat-lay + detail; make the model shot a cover-up
12964
- scene. Pet/object products: no such constraints.
13030
+ scene. Pet/object products: no such constraints. Applies to banner
13031
+ prompts too.
12965
13032
 
12966
13033
  ## Pipeline (after authoring)
12967
13034
 
12968
13035
  \`\`\`sh
12969
- zalify store validate . # lint this folder
13036
+ zalify brand validate # lint this folder (cwd)
12970
13037
  # create the Shopify dev store + Zalify workspace, then:
12971
13038
  zalify workspace set <workspace-slug>
12972
- zalify storefront create <slug> # repo + Vercel + domain + token + scaffold
12973
- zalify shopify import <this-dir> --prune
12974
- zalify images generate <this-dir>
12975
- zalify shopify upload-images <this-dir>
13039
+ zalify site create <slug> # hosted site: repo + deploy + domain + token
13040
+ zalify brand push --prune # catalog → your Shopify store
13041
+ zalify brand images generate # create the manifest's shots
13042
+ zalify brand images attach # attach them to the products
12976
13043
  \`\`\`
12977
13044
  `;
12978
13045
  var BRAND_MD = (name) => `# ${name} — brand voice guide
@@ -13059,6 +13126,7 @@ var CATALOG_JSON = (vendor, currency) => JSON.stringify({
13059
13126
  var MANIFEST_JSON = JSON.stringify({
13060
13127
  style: "<shared prefix for packshot/detail prompts: photography style, background, light, 'no text, no watermark. Square 1:1 composition.'>",
13061
13128
  modelStyle: "<shared prefix for type:'model' lifestyle prompts — scene, mood, framing. Mind the prompt-safety rules in AUTHORING.md for human models.>",
13129
+ bannerStyle: "<shared prefix for type:'banner' theme imagery — wide cinematic scene, no text or logos, calm negative space on one side for overlaid copy.>",
13062
13130
  images: [
13063
13131
  {
13064
13132
  handle: "example-product",
@@ -13078,11 +13146,25 @@ var MANIFEST_JSON = JSON.stringify({
13078
13146
  type: "model",
13079
13147
  alt: "The Example Product in use",
13080
13148
  prompt: "<lifestyle prompt: who/what uses it, where, light>"
13149
+ },
13150
+ {
13151
+ file: "hero-1.png",
13152
+ type: "banner",
13153
+ size: "1536x1024",
13154
+ alt: "Hero banner: the brand's world in one wide scene",
13155
+ prompt: "<wide hero scene prompt: setting, subject, light — no text, negative space on one side>"
13156
+ },
13157
+ {
13158
+ file: "hero-1-mobile.png",
13159
+ type: "banner",
13160
+ size: "1024x1536",
13161
+ alt: "Hero banner, portrait crop for mobile",
13162
+ prompt: "<same scene, portrait composition — subject centered, negative space above>"
13081
13163
  }
13082
13164
  ]
13083
13165
  }, null, 2) + `
13084
13166
  `;
13085
- function storeInit(dir2, opts) {
13167
+ function brandInit(dir2 = ".", opts = {}) {
13086
13168
  const target = resolve6(dir2);
13087
13169
  const slug = basename4(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
13088
13170
  if (existsSync7(target) && readdirSync3(target).length > 0) {
@@ -13091,20 +13173,21 @@ function storeInit(dir2, opts) {
13091
13173
  const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
13092
13174
  const currency = (opts.currency ?? "USD").toUpperCase();
13093
13175
  mkdirSync3(join7(target, "images"), { recursive: true });
13094
- writeFileSync6(join7(target, "AUTHORING.md"), AUTHORING_MD);
13095
- writeFileSync6(join7(target, "brand.md"), BRAND_MD(name));
13096
- writeFileSync6(join7(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
13097
- writeFileSync6(join7(target, "catalog.json"), CATALOG_JSON(name, currency));
13098
- writeFileSync6(join7(target, "images", "manifest.json"), MANIFEST_JSON);
13099
- console.log(`Scaffolded store data in ${dir2}:`);
13176
+ writeFileSync7(join7(target, "AUTHORING.md"), AUTHORING_MD);
13177
+ writeFileSync7(join7(target, "brand.md"), BRAND_MD(name));
13178
+ writeFileSync7(join7(target, "store.json"), STORE_JSON(name, slug, opts.vertical ?? "<vertical>", currency));
13179
+ writeFileSync7(join7(target, "catalog.json"), CATALOG_JSON(name, currency));
13180
+ writeFileSync7(join7(target, "images", "manifest.json"), MANIFEST_JSON);
13181
+ console.log(`Scaffolded brand data in ${dir2}:`);
13100
13182
  for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
13101
13183
  console.log(` + ${f}`);
13102
13184
  }
13103
13185
  console.log(`
13104
13186
  Author in this order: brand.md → catalog.json → images/manifest.json` + `
13105
- (rules + worked examples in AUTHORING.md; lint with \`zalify store validate ${dir2}\`)`);
13187
+ (rules + worked examples in AUTHORING.md; lint with \`zalify brand validate ${dir2}\`)`);
13106
13188
  }
13107
- function storeValidate(dir2) {
13189
+ var MANIFEST_SIZES = new Set(["1024x1024", "1536x1024", "1024x1536"]);
13190
+ function brandValidate(dir2 = ".") {
13108
13191
  const target = resolve6(dir2);
13109
13192
  const problems = [];
13110
13193
  const warnings = [];
@@ -13215,12 +13298,24 @@ function storeValidate(dir2) {
13215
13298
  problems.push(`${where}: duplicate file`);
13216
13299
  files.add(entry.file);
13217
13300
  }
13218
- if (entry.handle && handles.size && !handles.has(entry.handle)) {
13301
+ if (entry.type === "banner") {
13302
+ if (entry.handle) {
13303
+ warnings.push(`${where}: banner entries take no handle (ignored by attach)`);
13304
+ }
13305
+ } else if (!entry.handle) {
13306
+ problems.push(`${where}: handle required (only type "banner" entries omit it)`);
13307
+ } else if (handles.size && !handles.has(entry.handle)) {
13219
13308
  problems.push(`${where}: handle "${entry.handle}" not in catalog.json`);
13220
13309
  }
13310
+ if (entry.size && !MANIFEST_SIZES.has(entry.size)) {
13311
+ problems.push(`${where}: size must be one of ${[...MANIFEST_SIZES].join(", ")}`);
13312
+ }
13221
13313
  if (entry.type === "model" && (!manifest.modelStyle || manifest.modelStyle.startsWith("<"))) {
13222
13314
  warnings.push("images/manifest.json: modelStyle prefix not filled in (model shots present)");
13223
13315
  }
13316
+ if (entry.type === "banner" && (!manifest.bannerStyle || manifest.bannerStyle.startsWith("<"))) {
13317
+ warnings.push("images/manifest.json: bannerStyle prefix not filled in (banner entries present)");
13318
+ }
13224
13319
  }
13225
13320
  }
13226
13321
  for (const w of [...new Set(warnings)])
@@ -13228,7 +13323,7 @@ function storeValidate(dir2) {
13228
13323
  if (problems.length) {
13229
13324
  for (const p of problems)
13230
13325
  console.log(` ✗ ${p}`);
13231
- throw new Error(`${problems.length} problem(s) — fix and re-run \`zalify store validate\``);
13326
+ throw new Error(`${problems.length} problem(s) — fix and re-run \`zalify brand validate\``);
13232
13327
  }
13233
13328
  console.log(`✓ valid: ${handles.size} product(s), ${catalog?.collections?.length ?? 0} collection(s), ${manifest?.images?.length ?? 0} image entries` + (warnings.length ? ` — ${warnings.length} warning(s)` : ""));
13234
13329
  }
@@ -13244,7 +13339,78 @@ try {
13244
13339
  });
13245
13340
  } catch {}
13246
13341
  var program2 = new Command;
13247
- program2.name("zalify").description("Zalify CLI - command-line interface for Zalify").version(pkg.version, "-v, --version", "output the current version");
13342
+ program2.name("zalify").description("Zalify CLI author a brand, launch its site, keep the theme current").version(pkg.version, "-v, --version", "output the current version");
13343
+ program2.addHelpText("after", `
13344
+ The golden path (new brand, start to live):
13345
+ 1. zalify brand init stores/acme author the data (no login needed)
13346
+ 2. zalify login && zalify workspace set <workspace>
13347
+ 3. zalify site create acme repo + hosting + domain + deploy
13348
+ 4. cd stores/acme && zalify brand push --prune
13349
+ 5. zalify brand images generate && zalify brand images attach
13350
+ 6. zalify theme status | upgrade stay current, keep your edits
13351
+ `);
13352
+ var moved = (to) => console.log(`(moved — this is now \`zalify ${to}\`)`);
13353
+ program2.command("login").description("Authenticate via the browser (one login covers all your workspaces)").option("--app-url <url>", "Zalify app origin (default: https://app.zalify.com)").action(async (options) => {
13354
+ await login(options);
13355
+ });
13356
+ program2.command("logout").description("Delete the stored CLI credentials").action(() => logout());
13357
+ program2.command("whoami").description("Verify the stored key and show workspace, plan, and key details").action(async () => {
13358
+ await whoami();
13359
+ });
13360
+ var workspace = program2.command("workspace").description("Active workspace — which Shopify store your commands target");
13361
+ workspace.command("list", { isDefault: true }).description("List authorized workspaces (* = active)").action(async () => {
13362
+ await workspaceList();
13363
+ });
13364
+ workspace.command("set <slug-or-id>").description("Switch the active workspace (local, no re-login)").action(async (slugOrId) => {
13365
+ await workspaceSet(slugOrId);
13366
+ });
13367
+ var brand = program2.command("brand").description("Brand data — author the catalog, voice, and imagery; push it to your Shopify store");
13368
+ brand.command("init [dir]").description("Scaffold a brand folder (catalog, voice guide, image manifest + AUTHORING.md rules); no login needed").option("--name <name>", "brand name (default: derived from the directory)").option("--vertical <vertical>", "e.g. womens-fast-fashion, pet-supplies").option("--currency <code>", "ISO currency", "USD").action((dir2, options) => {
13369
+ brandInit(dir2, options);
13370
+ });
13371
+ brand.command("validate [dir]").description("Lint the brand folder against the authoring rules").action((dir2) => {
13372
+ brandValidate(dir2);
13373
+ });
13374
+ brand.command("push [dir]").description("Push catalog.json to your connected Shopify store (products, smart collections, publish; idempotent)").option("--prune", "delete store products (by catalog vendor) absent from catalog.json").option("--site <slug>", "refresh only this site afterwards (default: every site matching the brand)").action(async (dir2, options) => {
13375
+ await shopifyImport(dir2 ?? ".", options);
13376
+ });
13377
+ var brandImages = brand.command("images").description("The brand's imagery pipeline: generate → attach → push/pull masters");
13378
+ brandImages.command("generate [dir]").description("Generate the manifest's missing images on Zalify infrastructure (resumable)").action(async (dir2) => {
13379
+ await imagesGenerate(dir2 ?? ".");
13380
+ });
13381
+ brandImages.command("attach [dir]").description("Attach generated images to their Shopify products per the manifest (skips already-attached)").option("--site <slug>", "refresh only this site afterwards").action(async (dir2, options) => {
13382
+ await shopifyUploadImages(dir2 ?? ".", options);
13383
+ });
13384
+ brandImages.command("push [dir]").description("Upload image masters to the Zalify asset library (sha256 dedup, writes assets.json)").action(async (dir2) => {
13385
+ await assetsPush(dir2 ?? ".");
13386
+ });
13387
+ brandImages.command("pull [dir]").description("Download masters listed in images/assets.json (fresh-clone restore)").action(async (dir2) => {
13388
+ await assetsPull(dir2 ?? ".");
13389
+ });
13390
+ var site = program2.command("site").description("Hosted sites — Zalify provisions the repo, deployment, and domain");
13391
+ site.command("create <dir>").description("One-command launch: provision repo + deployment + domain + storefront token, scaffold locally, push, go live").option("-t, --theme <name>", "theme", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--to <version>", "theme version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init/push").action(async (dir2, options) => {
13392
+ await siteCreate(dir2, options);
13393
+ });
13394
+ site.command("list", { isDefault: true }).description("List the workspace's hosted sites").action(async () => {
13395
+ await siteList();
13396
+ });
13397
+ site.command("refresh [slug-or-dir]").description("Refresh the live site's data cache (defaults to the site linked to cwd)").action(async (slugOrDir) => {
13398
+ await siteRefresh(slugOrDir);
13399
+ });
13400
+ site.command("open [slug-or-dir]").description("Open the live site in your browser (defaults to the site linked to cwd)").action(async (slugOrDir) => {
13401
+ await siteOpen(slugOrDir);
13402
+ });
13403
+ var theme = program2.command("theme").description("Theme source code — scaffold a copy and upgrade it without losing your edits");
13404
+ theme.command("create <dir>").description("Scaffold only — no Zalify hosting (self-host path; `site create` is the fully-hosted launch)").option("-t, --template <name>", "liquid | hydrogen | nextjs", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--store-domain <domain>", "your-store.myshopify.com (default: mock.shop demo data)").option("--storefront-token <token>", "public Storefront API access token").option("--to <version>", "theme version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init + initial commit").option("--tarball <path>", "use a local @zalify/theme-templates .tgz instead of npm").action(async (dir2, options) => {
13405
+ await themeCreate(dir2, options);
13406
+ });
13407
+ theme.command("status [dir]").description("Which theme files have you edited? (checksums vs .zalify/theme.json)").action((dir2) => {
13408
+ themeStatus(dir2);
13409
+ });
13410
+ theme.command("upgrade [dir]").description("Move to a newer theme version: untouched files overwritten, edited files 3-way-merged, theme/ + .env never touched").option("--to <version>", "target theme version (default: latest)").option("--dry-run", "classify and report without writing anything").option("--install", "run install when dependencies changed").option("--tarball <path>", "target version as a local .tgz (testing)").option("--base-tarball <path>", "base version as a local .tgz (testing)").action(async (dir2, options) => {
13411
+ await themeUpgrade(dir2, options);
13412
+ });
13413
+ program2.command("self-update").description("Update the CLI to the latest version (auto-detects pnpm/bun/npm)").action(() => selfUpdate(pkg.version));
13248
13414
  program2.command("version").description("Print Zalify CLI version and check for updates").action(async () => {
13249
13415
  console.log(`Zalify CLI version: ${pkg.version}`);
13250
13416
  const isNewer = (a, b) => {
@@ -13261,69 +13427,54 @@ program2.command("version").description("Print Zalify CLI version and check for
13261
13427
  const update = await notifier.fetchInfo();
13262
13428
  if (update && isNewer(update.latest, update.current)) {
13263
13429
  console.log(`
13264
- A new version (${update.latest}) is available. Run "npm i -g @zalify/cli@latest" to update.`);
13430
+ A new version (${update.latest}) is available. Run "zalify self-update" to update.`);
13265
13431
  }
13266
13432
  } catch {}
13267
13433
  });
13268
- program2.command("self-update").description("Update the CLI to the latest version (auto-detects pnpm/bun/npm)").action(() => selfUpdate(pkg.version));
13269
- program2.command("login").description("Authenticate via the browser and store a workspace API key").option("--app-url <url>", "Zalify app origin (default: https://app.zalify.com)").action(async (options) => {
13270
- await login(options);
13434
+ var legacyStore = program2.command("store", { hidden: true });
13435
+ legacyStore.command("init <dir>").option("--name <name>").option("--vertical <vertical>").option("--currency <code>").action((dir2, options) => {
13436
+ moved("brand init");
13437
+ brandInit(dir2, options);
13271
13438
  });
13272
- program2.command("logout").description("Delete the stored CLI credentials").action(() => logout());
13273
- program2.command("whoami").description("Verify the stored key and show workspace, plan, and key details").action(async () => {
13274
- await whoami();
13275
- });
13276
- var workspace = program2.command("workspace").description("List authorized workspaces or switch the active one");
13277
- workspace.command("list", { isDefault: true }).description("List authorized workspaces (* = active)").action(async () => {
13278
- await workspaceList();
13439
+ legacyStore.command("validate <dir>").action((dir2) => {
13440
+ moved("brand validate");
13441
+ brandValidate(dir2);
13279
13442
  });
13280
- workspace.command("set <slug-or-id>").description("Switch the active workspace (local, no re-login)").action(async (slugOrId) => {
13281
- await workspaceSet(slugOrId);
13282
- });
13283
- var images = program2.command("images").description("Generate images on Zalify infrastructure");
13284
- images.command("generate <store-dir>").description("Generate missing manifest images server-side (results land in the asset library and are downloaded locally)").action(async (storeDir) => {
13285
- await imagesGenerate(storeDir);
13286
- });
13287
- var shopify = program2.command("shopify").description("Push catalogs and images to the workspace's connected Shopify store");
13288
- shopify.command("import <store-dir>").description("Import <store-dir>/catalog.json into the connected Shopify store (products, smart collections, Online Store publish; idempotent by handle)").option("--prune", "delete store products (by catalog vendor) absent from catalog.json").action(async (storeDir, options) => {
13443
+ var legacyShopify = program2.command("shopify", { hidden: true });
13444
+ legacyShopify.command("import <store-dir>").option("--prune").action(async (storeDir, options) => {
13445
+ moved("brand push");
13289
13446
  await shopifyImport(storeDir, options);
13290
13447
  });
13291
- shopify.command("upload-images <store-dir>").description("Upload <store-dir>/images/* to matching products per images/manifest.json (skips files already attached)").action(async (storeDir) => {
13448
+ legacyShopify.command("upload-images <store-dir>").action(async (storeDir) => {
13449
+ moved("brand images attach");
13292
13450
  await shopifyUploadImages(storeDir);
13293
13451
  });
13294
- var store = program2.command("store").description("Author store data (catalog, brand voice, image manifest) — step zero, no login needed");
13295
- store.command("init <dir>").description("Scaffold a store-data folder: store.json, brand.md, catalog.json, images/manifest.json + AUTHORING.md (the rules)").option("--name <name>", "brand name (default: derived from the directory)").option("--vertical <vertical>", "e.g. womens-fast-fashion, pet-supplies").option("--currency <code>", "ISO currency", "USD").action((dir2, options) => {
13296
- storeInit(dir2, options);
13297
- });
13298
- store.command("validate <dir>").description("Lint the store-data folder against the authoring rules (schema, collection tags, sale pricing, image manifest)").action((dir2) => {
13299
- storeValidate(dir2);
13300
- });
13301
- var storefront = program2.command("storefront").description("Provision and manage fully-hosted standalone storefronts (repo + Vercel via Zalify)");
13302
- storefront.command("create <dir>").description("One-command launch: Zalify provisions the GitHub repo + Vercel project (env, domain, storefront token), then scaffolds locally and pushes").option("-t, --theme <name>", "theme", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--to <version>", "template version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init/push").action(async (dir2, options) => {
13303
- await storefrontCreate(dir2, options);
13304
- });
13305
- storefront.command("list", { isDefault: true }).description("List the workspace's provisioned storefronts").action(async () => {
13306
- await storefrontList();
13307
- });
13308
- storefront.command("revalidate <slug-or-store-dir>").description("Flush the storefront site's data cache (secret stays server-side)").action(async (slugOrDir) => {
13309
- await storefrontRevalidate(slugOrDir);
13452
+ var legacyImages = program2.command("images", { hidden: true });
13453
+ legacyImages.command("generate <store-dir>").action(async (storeDir) => {
13454
+ moved("brand images generate");
13455
+ await imagesGenerate(storeDir);
13310
13456
  });
13311
- var theme = program2.command("theme").description("Scaffold and upgrade standalone Zalify storefronts (Shopify-theme model)");
13312
- theme.command("create <dir>").description("Scaffold a standalone storefront from the published theme templates").option("-t, --template <name>", "liquid | hydrogen | nextjs", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--store-domain <domain>", "your-store.myshopify.com (default: mock.shop demo data)").option("--storefront-token <token>", "public Storefront API access token").option("--to <version>", "template version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init + initial commit").option("--tarball <path>", "use a local @zalify/theme-templates .tgz instead of npm").action(async (dir2, options) => {
13313
- await themeCreate(dir2, options);
13457
+ var legacyAssets = program2.command("assets", { hidden: true });
13458
+ legacyAssets.command("push <store-dir>").action(async (storeDir) => {
13459
+ moved("brand images push");
13460
+ await assetsPush(storeDir);
13314
13461
  });
13315
- theme.command("status [dir]").description("List theme-owned files edited since scaffolding (.zalify/theme.json checksums)").action((dir2) => {
13316
- themeStatus(dir2);
13462
+ legacyAssets.command("pull <store-dir>").action(async (storeDir) => {
13463
+ moved("brand images pull");
13464
+ await assetsPull(storeDir);
13317
13465
  });
13318
- theme.command("upgrade [dir]").description("Upgrade to a newer theme version: untouched files overwritten, edited files 3-way-merged, theme/ + .env never touched").option("--to <version>", "target template version (default: latest)").option("--dry-run", "classify and report without writing anything").option("--install", "run install when dependencies changed").option("--tarball <path>", "target version as a local .tgz (testing)").option("--base-tarball <path>", "base version as a local .tgz (testing)").action(async (dir2, options) => {
13319
- await themeUpgrade(dir2, options);
13466
+ var legacyStorefront = program2.command("storefront", { hidden: true });
13467
+ legacyStorefront.command("create <dir>").option("-t, --theme <name>", "", "nextjs").option("--editor").option("--to <version>").option("--no-install").option("--no-git").action(async (dir2, options) => {
13468
+ moved("site create");
13469
+ await siteCreate(dir2, options);
13320
13470
  });
13321
- var assets = program2.command("assets").description("Sync images with the Zalify asset library");
13322
- assets.command("push <store-dir>").description("Upload <store-dir>/images/*.png (sha256 dedup, writes assets.json)").action(async (storeDir) => {
13323
- await assetsPush(storeDir);
13471
+ legacyStorefront.command("list", { isDefault: true }).action(async () => {
13472
+ moved("site list");
13473
+ await siteList();
13324
13474
  });
13325
- assets.command("pull <store-dir>").description("Download files listed in <store-dir>/images/assets.json").action(async (storeDir) => {
13326
- await assetsPull(storeDir);
13475
+ legacyStorefront.command("revalidate <slug-or-store-dir>").action(async (slugOrDir) => {
13476
+ moved("site refresh");
13477
+ await siteRefresh(slugOrDir);
13327
13478
  });
13328
13479
  program2.parseAsync().catch((err) => {
13329
13480
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",