@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.
- package/dist/cli.js +332 -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
|
|
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(
|
|
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);
|