@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.
- package/dist/cli.js +295 -144
- 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/
|
|
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/
|
|
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
|
-
` + `
|
|
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
|
|
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 ?? "
|
|
12514
|
+
return new Error(json.error ?? "Site provisioning is not configured on the server.");
|
|
12515
12515
|
}
|
|
12516
|
-
return new Error(`
|
|
12516
|
+
return new Error(`sites API ${status}: ${JSON.stringify(json)}`);
|
|
12517
12517
|
}
|
|
12518
|
-
async function
|
|
12519
|
-
const
|
|
12520
|
-
method: "
|
|
12521
|
-
|
|
12522
|
-
|
|
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
|
|
12530
|
-
const
|
|
12531
|
-
|
|
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
|
|
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
|
|
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, "") || "
|
|
12562
|
-
console.log(`Provisioning
|
|
12563
|
-
const
|
|
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
|
|
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
|
|
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} —
|
|
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
|
|
12611
|
-
` + ` - zalify
|
|
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
|
|
12660
|
+
async function siteList() {
|
|
12614
12661
|
const auth = requireActive();
|
|
12615
|
-
const
|
|
12616
|
-
if (!
|
|
12617
|
-
console.log(`No
|
|
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
|
|
12621
|
-
const
|
|
12622
|
-
console.log(`${s.status === "READY" ? "✓" : s.status === "FAILED" ? "✗" : "…"} ${s.slug} ${
|
|
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
|
|
12626
|
-
const
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
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
|
|
12678
|
+
async function siteOpen(slugOrDir) {
|
|
12635
12679
|
const auth = requireActive();
|
|
12636
|
-
const
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
console.log(
|
|
12643
|
-
}
|
|
12644
|
-
async function
|
|
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
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
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
|
|
12652
|
-
|
|
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
|
|
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
|
|
12963
|
+
await maybeRefreshSites(auth, dir2, options.site);
|
|
12911
12964
|
}
|
|
12912
12965
|
|
|
12913
|
-
// src/
|
|
12914
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync7, writeFileSync as
|
|
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 = `#
|
|
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
|
|
12920
|
-
alone. Validate anytime with \`zalify
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
12973
|
-
zalify
|
|
12974
|
-
zalify images generate
|
|
12975
|
-
zalify
|
|
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
|
|
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
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
console.log(`Scaffolded
|
|
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
|
|
13187
|
+
(rules + worked examples in AUTHORING.md; lint with \`zalify brand validate ${dir2}\`)`);
|
|
13106
13188
|
}
|
|
13107
|
-
|
|
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.
|
|
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
|
|
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
|
|
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 "
|
|
13430
|
+
A new version (${update.latest}) is available. Run "zalify self-update" to update.`);
|
|
13265
13431
|
}
|
|
13266
13432
|
} catch {}
|
|
13267
13433
|
});
|
|
13268
|
-
program2.command("
|
|
13269
|
-
|
|
13270
|
-
|
|
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
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
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
|
-
|
|
13281
|
-
|
|
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
|
-
|
|
13448
|
+
legacyShopify.command("upload-images <store-dir>").action(async (storeDir) => {
|
|
13449
|
+
moved("brand images attach");
|
|
13292
13450
|
await shopifyUploadImages(storeDir);
|
|
13293
13451
|
});
|
|
13294
|
-
var
|
|
13295
|
-
|
|
13296
|
-
|
|
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
|
|
13312
|
-
|
|
13313
|
-
|
|
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
|
-
|
|
13316
|
-
|
|
13462
|
+
legacyAssets.command("pull <store-dir>").action(async (storeDir) => {
|
|
13463
|
+
moved("brand images pull");
|
|
13464
|
+
await assetsPull(storeDir);
|
|
13317
13465
|
});
|
|
13318
|
-
|
|
13319
|
-
|
|
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
|
-
|
|
13322
|
-
|
|
13323
|
-
await
|
|
13471
|
+
legacyStorefront.command("list", { isDefault: true }).action(async () => {
|
|
13472
|
+
moved("site list");
|
|
13473
|
+
await siteList();
|
|
13324
13474
|
});
|
|
13325
|
-
|
|
13326
|
-
|
|
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)}`);
|