@zalify/cli 0.9.1 → 0.10.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 +332 -2
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -11293,7 +11293,7 @@ function updateNotifier(options) {
11293
11293
 
11294
11294
  // src/cli.ts
11295
11295
  import { createRequire as createRequire2 } from "node:module";
11296
- import { dirname as dirname2, join as join7 } from "node:path";
11296
+ import { dirname as dirname2, join as join8 } from "node:path";
11297
11297
  import { fileURLToPath as fileURLToPath3 } from "node:url";
11298
11298
 
11299
11299
  // src/auth.ts
@@ -12910,10 +12910,333 @@ async function shopifyUploadImages(storeDir) {
12910
12910
  await maybeRevalidateStorefront(auth, dir2);
12911
12911
  }
12912
12912
 
12913
+ // src/store.ts
12914
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
12915
+ import { basename as basename4, join as join7, resolve as resolve6 } from "node:path";
12916
+ var AUTHORING_MD = `# Store authoring guide
12917
+
12918
+ 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 .\`.
12921
+
12922
+ ## Files
12923
+
12924
+ | File | Role |
12925
+ | ---- | ---- |
12926
+ | store.json | identity: name, slug, vertical, currency; storeDomain filled in once the Shopify dev store exists |
12927
+ | brand.md | brand voice guide — tone, audience, naming, price band. Written FIRST; everything else follows it |
12928
+ | catalog.json | canonical catalog (products + collections) — the single source the pipeline imports |
12929
+ | images/manifest.json | one entry per shot + shared style/modelStyle art-direction prefixes |
12930
+ | images/*.png | generated masters (NOT committed — they live in the Zalify asset library; images/assets.json is the committed index) |
12931
+
12932
+ ## catalog.json rules
12933
+
12934
+ - 10–14 products is the sweet spot for a demo store; every product needs
12935
+ \`handle\` (unique, kebab-case, no SKU-speak), \`title\`, \`descriptionHtml\`
12936
+ (brand voice; simple HTML: p/ul/li/em only), \`type\`, \`tags\`, \`variants\`.
12937
+ - **Collections are smart collections driven by tags**: a product joins
12938
+ collection \`x\` by carrying tag \`collection:x\`, and the collection's
12939
+ \`rule\` is \`{ "tag": "collection:x" }\`. Every product belongs to at
12940
+ least one collection.
12941
+ - **Mandatory collections**: \`best-sellers\` and \`sale\`. Sale products
12942
+ carry a real \`compareAtPrice\` AND the \`collection:sale\` tag.
12943
+ - \`options\` is 0–3 option names; each variant's \`options\` array matches
12944
+ that length and order. Single-variant products omit \`options\` and give
12945
+ one variant with \`"options": []\`.
12946
+ - Prices are strings with 2 decimals, in the store currency. Honest
12947
+ markdowns only (15–25% off typical).
12948
+ - Colors and variants get evocative names per brand.md — never "Gray 02".
12949
+
12950
+ ## images/manifest.json rules
12951
+
12952
+ - Three shots per hero product: packshot (\`<handle>.png\`), macro detail
12953
+ (\`<handle>-detail.png\`), lifestyle/model (\`<handle>-model.png\`,
12954
+ \`"type": "model"\`). ~33–36 entries total. Every \`handle\` must exist in
12955
+ catalog.json; every entry needs \`alt\` text.
12956
+ - \`style\` prefixes packshot/detail prompts; \`modelStyle\` prefixes
12957
+ \`type: "model"\` prompts. Keep prompts concrete: materials, colors,
12958
+ camera angle, light.
12959
+ - **Prompt safety (gpt-image-2 rejects [sexual] otherwise)** — for
12960
+ human-model shots: say "woman"/"man", never "young …"; keep fitted
12961
+ garments layered or covered (blazer / cardigan / cover-up); crop at
12962
+ the chin or shoot from behind; prefer wider framing. Swimwear: show
12963
+ the garment via flat-lay + detail; make the model shot a cover-up
12964
+ scene. Pet/object products: no such constraints.
12965
+
12966
+ ## Pipeline (after authoring)
12967
+
12968
+ \`\`\`sh
12969
+ zalify store validate . # lint this folder
12970
+ # create the Shopify dev store + Zalify workspace, then:
12971
+ 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>
12976
+ \`\`\`
12977
+ `;
12978
+ var BRAND_MD = (name) => `# ${name} — brand voice guide
12979
+
12980
+ ## Who we are
12981
+
12982
+ <2–3 sentences: what the brand makes, for whom, and the feeling it sells.>
12983
+
12984
+ ## Audience
12985
+
12986
+ <Who buys: age band, habits, what they care about. Name the customer the
12987
+ way the brand would ("pet parents", not "owners").>
12988
+
12989
+ ## Tone
12990
+
12991
+ - <3–5 bullets: adjectives with teeth, plus what the brand NEVER sounds like.>
12992
+ - <How to address the reader (second person? playful? expert-calm?).>
12993
+
12994
+ ## Naming conventions
12995
+
12996
+ - <Pattern for product names, with 2–3 examples.>
12997
+ - <How colors/variants are named (evocative, never "Gray 02").>
12998
+
12999
+ ## Price band
13000
+
13001
+ - <Floor–ceiling and what anchors each end. Markdown policy.>
13002
+
13003
+ ## Quick sniff test
13004
+
13005
+ <One sentence: how to tell a line of copy is (or isn't) this brand.>
13006
+ `;
13007
+ var STORE_JSON = (name, slug, vertical, currency) => JSON.stringify({
13008
+ name,
13009
+ slug,
13010
+ vertical,
13011
+ currency,
13012
+ storeDomain: null,
13013
+ notes: "Standalone-copy deployment — fill storeDomain when the Shopify dev store exists; no siteUrl on purpose."
13014
+ }, null, 2) + `
13015
+ `;
13016
+ var CATALOG_JSON = (vendor, currency) => JSON.stringify({
13017
+ vendor,
13018
+ currency,
13019
+ products: [
13020
+ {
13021
+ handle: "example-product",
13022
+ title: "The Example Product",
13023
+ descriptionHtml: "<p>Two short paragraphs in the brand voice: lead with the moment the customer uses it, then the practical facts.</p><ul><li>Material / composition</li><li>Care instructions</li><li>Size guidance</li></ul>",
13024
+ type: "Category",
13025
+ tags: ["example", "collection:best-sellers"],
13026
+ options: ["Color"],
13027
+ variants: [
13028
+ {
13029
+ options: ["Oat Cream"],
13030
+ sku: "EXM-001-OAT",
13031
+ price: "29.00",
13032
+ compareAtPrice: null,
13033
+ grams: 500,
13034
+ inventory: 50
13035
+ }
13036
+ ],
13037
+ seo: {
13038
+ title: "The Example Product — Brand",
13039
+ description: "One-sentence meta description in the brand voice."
13040
+ }
13041
+ }
13042
+ ],
13043
+ collections: [
13044
+ {
13045
+ handle: "best-sellers",
13046
+ title: "Best Sellers",
13047
+ descriptionHtml: "<p>The ones everyone comes back for.</p>",
13048
+ rule: { tag: "collection:best-sellers" }
13049
+ },
13050
+ {
13051
+ handle: "sale",
13052
+ title: "Sale",
13053
+ descriptionHtml: "<p>Real markdowns on favorites.</p>",
13054
+ rule: { tag: "collection:sale" }
13055
+ }
13056
+ ]
13057
+ }, null, 2) + `
13058
+ `;
13059
+ var MANIFEST_JSON = JSON.stringify({
13060
+ style: "<shared prefix for packshot/detail prompts: photography style, background, light, 'no text, no watermark. Square 1:1 composition.'>",
13061
+ modelStyle: "<shared prefix for type:'model' lifestyle prompts — scene, mood, framing. Mind the prompt-safety rules in AUTHORING.md for human models.>",
13062
+ images: [
13063
+ {
13064
+ handle: "example-product",
13065
+ file: "example-product.png",
13066
+ alt: "The Example Product in Oat Cream, front three-quarter view",
13067
+ prompt: "<concrete packshot prompt: the product, materials, colors, camera angle>"
13068
+ },
13069
+ {
13070
+ handle: "example-product",
13071
+ file: "example-product-detail.png",
13072
+ alt: "Macro detail of the Example Product's texture",
13073
+ prompt: "<extreme macro close-up prompt: texture, seam, hardware>"
13074
+ },
13075
+ {
13076
+ handle: "example-product",
13077
+ file: "example-product-model.png",
13078
+ type: "model",
13079
+ alt: "The Example Product in use",
13080
+ prompt: "<lifestyle prompt: who/what uses it, where, light>"
13081
+ }
13082
+ ]
13083
+ }, null, 2) + `
13084
+ `;
13085
+ function storeInit(dir2, opts) {
13086
+ const target = resolve6(dir2);
13087
+ const slug = basename4(target).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "");
13088
+ if (existsSync7(target) && readdirSync3(target).length > 0) {
13089
+ throw new Error(`${dir2} already exists and is not empty`);
13090
+ }
13091
+ const name = opts.name ?? slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
13092
+ const currency = (opts.currency ?? "USD").toUpperCase();
13093
+ 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}:`);
13100
+ for (const f of ["AUTHORING.md", "brand.md", "store.json", "catalog.json", "images/manifest.json"]) {
13101
+ console.log(` + ${f}`);
13102
+ }
13103
+ console.log(`
13104
+ Author in this order: brand.md → catalog.json → images/manifest.json` + `
13105
+ (rules + worked examples in AUTHORING.md; lint with \`zalify store validate ${dir2}\`)`);
13106
+ }
13107
+ function storeValidate(dir2) {
13108
+ const target = resolve6(dir2);
13109
+ const problems = [];
13110
+ const warnings = [];
13111
+ const readJson2 = (rel) => {
13112
+ const path9 = join7(target, rel);
13113
+ if (!existsSync7(path9)) {
13114
+ problems.push(`missing ${rel}`);
13115
+ return null;
13116
+ }
13117
+ try {
13118
+ return JSON.parse(readFileSync7(path9, "utf8"));
13119
+ } catch (err) {
13120
+ problems.push(`${rel}: invalid JSON — ${err instanceof Error ? err.message : err}`);
13121
+ return null;
13122
+ }
13123
+ };
13124
+ if (!existsSync7(join7(target, "brand.md")))
13125
+ problems.push("missing brand.md");
13126
+ const store = readJson2("store.json");
13127
+ if (store && (!store.name || !store.slug))
13128
+ problems.push("store.json: name and slug are required");
13129
+ const catalog = readJson2("catalog.json");
13130
+ const handles = new Set;
13131
+ if (catalog) {
13132
+ const products = catalog.products ?? [];
13133
+ if (!products.length)
13134
+ problems.push("catalog.json: no products");
13135
+ if (products.some((p) => p.handle === "example-product")) {
13136
+ warnings.push("catalog.json still contains the example-product stub");
13137
+ }
13138
+ const collectionTags = new Set((catalog.collections ?? []).map((c) => c.rule?.tag).filter(Boolean));
13139
+ for (const c of ["best-sellers", "sale"]) {
13140
+ if (!(catalog.collections ?? []).some((col) => col.handle === c)) {
13141
+ problems.push(`catalog.json: mandatory collection "${c}" missing`);
13142
+ }
13143
+ }
13144
+ for (const p of products) {
13145
+ const where = `product "${p.handle ?? p.title ?? "?"}"`;
13146
+ if (!p.handle || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(p.handle)) {
13147
+ problems.push(`${where}: handle must be kebab-case`);
13148
+ } else if (handles.has(p.handle)) {
13149
+ problems.push(`${where}: duplicate handle`);
13150
+ } else {
13151
+ handles.add(p.handle);
13152
+ }
13153
+ if (!p.title || !p.descriptionHtml || !p.type) {
13154
+ problems.push(`${where}: title, descriptionHtml, and type are required`);
13155
+ }
13156
+ const tags = p.tags ?? [];
13157
+ const colTags = tags.filter((t) => t.startsWith("collection:"));
13158
+ if (!colTags.length)
13159
+ problems.push(`${where}: no collection:<handle> tag`);
13160
+ for (const t of colTags) {
13161
+ if (!collectionTags.has(t)) {
13162
+ problems.push(`${where}: tag "${t}" matches no collection rule`);
13163
+ }
13164
+ }
13165
+ const optionCount = p.options?.length ?? 0;
13166
+ if (optionCount > 3)
13167
+ problems.push(`${where}: more than 3 options`);
13168
+ if (!p.variants?.length) {
13169
+ problems.push(`${where}: at least one variant required`);
13170
+ } else {
13171
+ const hasSale = tags.includes("collection:sale");
13172
+ let anyCompareAt = false;
13173
+ for (const v of p.variants) {
13174
+ if ((v.options?.length ?? 0) !== optionCount) {
13175
+ problems.push(`${where}: variant options length must match options (${optionCount})`);
13176
+ }
13177
+ if (!v.price || !/^\d+\.\d{2}$/.test(v.price)) {
13178
+ problems.push(`${where}: price must be a string with 2 decimals`);
13179
+ }
13180
+ if (v.compareAtPrice != null) {
13181
+ anyCompareAt = true;
13182
+ if (!/^\d+\.\d{2}$/.test(v.compareAtPrice)) {
13183
+ problems.push(`${where}: compareAtPrice must be a string with 2 decimals`);
13184
+ } else if (v.price && Number(v.compareAtPrice) <= Number(v.price)) {
13185
+ problems.push(`${where}: compareAtPrice must exceed price`);
13186
+ }
13187
+ }
13188
+ }
13189
+ if (hasSale && !anyCompareAt) {
13190
+ problems.push(`${where}: in collection:sale but no variant has compareAtPrice`);
13191
+ }
13192
+ if (anyCompareAt && !hasSale) {
13193
+ warnings.push(`${where}: has compareAtPrice but not tagged collection:sale`);
13194
+ }
13195
+ }
13196
+ }
13197
+ }
13198
+ const manifest = readJson2("images/manifest.json");
13199
+ if (manifest) {
13200
+ const images = manifest.images ?? [];
13201
+ if (!images.length)
13202
+ problems.push("images/manifest.json: no images");
13203
+ if (!manifest.style || manifest.style.startsWith("<")) {
13204
+ warnings.push("images/manifest.json: style prefix not filled in");
13205
+ }
13206
+ const files = new Set;
13207
+ for (const entry of images) {
13208
+ const where = `image "${entry.file ?? "?"}"`;
13209
+ if (!entry.file || !entry.prompt)
13210
+ problems.push(`${where}: file and prompt required`);
13211
+ if (!entry.alt)
13212
+ problems.push(`${where}: alt text required`);
13213
+ if (entry.file) {
13214
+ if (files.has(entry.file))
13215
+ problems.push(`${where}: duplicate file`);
13216
+ files.add(entry.file);
13217
+ }
13218
+ if (entry.handle && handles.size && !handles.has(entry.handle)) {
13219
+ problems.push(`${where}: handle "${entry.handle}" not in catalog.json`);
13220
+ }
13221
+ if (entry.type === "model" && (!manifest.modelStyle || manifest.modelStyle.startsWith("<"))) {
13222
+ warnings.push("images/manifest.json: modelStyle prefix not filled in (model shots present)");
13223
+ }
13224
+ }
13225
+ }
13226
+ for (const w of [...new Set(warnings)])
13227
+ console.log(` ! ${w}`);
13228
+ if (problems.length) {
13229
+ for (const p of problems)
13230
+ console.log(` ✗ ${p}`);
13231
+ throw new Error(`${problems.length} problem(s) — fix and re-run \`zalify store validate\``);
13232
+ }
13233
+ 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
+ }
13235
+
12913
13236
  // src/cli.ts
12914
13237
  var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
12915
13238
  var require2 = createRequire2(import.meta.url);
12916
- var pkg = require2(join7(__dirname4, "..", "package.json"));
13239
+ var pkg = require2(join8(__dirname4, "..", "package.json"));
12917
13240
  try {
12918
13241
  updateNotifier({ pkg }).notify({
12919
13242
  isGlobal: true,
@@ -12968,6 +13291,13 @@ shopify.command("import <store-dir>").description("Import <store-dir>/catalog.js
12968
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) => {
12969
13292
  await shopifyUploadImages(storeDir);
12970
13293
  });
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
+ });
12971
13301
  var storefront = program2.command("storefront").description("Provision and manage fully-hosted standalone storefronts (repo + Vercel via Zalify)");
12972
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) => {
12973
13303
  await storefrontCreate(dir2, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",