@zalify/cli 0.8.0 → 0.9.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 +492 -315
  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 join6 } from "node:path";
11296
+ import { dirname as dirname2, join as join7 } from "node:path";
11297
11297
  import { fileURLToPath as fileURLToPath3 } from "node:url";
11298
11298
 
11299
11299
  // src/auth.ts
@@ -12061,272 +12061,24 @@ async function imagesGenerate(storeDir) {
12061
12061
  }
12062
12062
 
12063
12063
  // src/shopify.ts
12064
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
12065
- import { basename, join as join4, resolve as resolve3 } from "node:path";
12066
- function proxyError(auth, status, json) {
12067
- if (json.code === "SCOPES_MISSING") {
12068
- const granted = new Set(json.grantedScopes ?? []);
12069
- const missing = (json.requiredScopes ?? []).filter((s) => !granted.has(s));
12070
- return new Error(`Your Shopify connection is missing newly added permissions: ${missing.join(", ") || "(unknown)"}.
12071
- ` + ` Reconnect the store (one-time approval): ${json.reconnectUrl ?? `${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`}
12072
- ` + ` Then re-run this command.`);
12073
- }
12074
- if (status === 404) {
12075
- return new Error("Shopify Admin proxy not found (404) — the /api/shopify/admin endpoint " + "is probably not deployed yet on this Zalify app. Try again after the next deploy.");
12076
- }
12077
- if (status === 402 || json.code === "UPGRADE_REQUIRED") {
12078
- return new Error(`${json.error ?? "Paid plan required."}
12079
- Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
12080
- }
12081
- if (status === 409) {
12082
- return new Error(`${json.error ?? "No Shopify store is connected to this workspace."}
12083
- ` + ` Connect one: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`);
12084
- }
12085
- return new Error(`shopify/admin ${status}: ${JSON.stringify(json)}`);
12086
- }
12087
- async function gql(auth, query, variables = {}) {
12088
- const res = await fetch(`${auth.appUrl}/api/shopify/admin`, {
12089
- method: "POST",
12090
- headers: {
12091
- "Content-Type": "application/json",
12092
- Authorization: `Bearer ${auth.key}`
12093
- },
12094
- body: JSON.stringify({ workspaceId: auth.workspaceId, query, variables })
12095
- });
12096
- const json = await res.json().catch(() => ({}));
12097
- if (!res.ok)
12098
- throw proxyError(auth, res.status, json);
12099
- if (json.errors)
12100
- throw new Error(JSON.stringify(json.errors));
12101
- if (!json.data)
12102
- throw new Error(`shopify/admin: empty response`);
12103
- return json.data;
12104
- }
12105
- function readJson(path9, label) {
12106
- if (!existsSync4(path9)) {
12107
- throw new Error(`No ${label} at ${path9}`);
12108
- }
12109
- return JSON.parse(readFileSync4(path9, "utf8"));
12110
- }
12111
- function joinErrors(errs) {
12112
- return errs.map((e) => e.message).join("; ");
12113
- }
12114
- var PRODUCT_SET = `
12115
- mutation productSet($input: ProductSetInput!) {
12116
- productSet(input: $input, synchronous: true) {
12117
- product { id handle }
12118
- userErrors { field message }
12119
- }
12120
- }`;
12121
- var COLLECTION_CREATE = `
12122
- mutation collectionCreate($input: CollectionInput!) {
12123
- collectionCreate(input: $input) {
12124
- collection { id handle }
12125
- userErrors { field message }
12126
- }
12127
- }`;
12128
- async function findProductId(auth, handle) {
12129
- const data = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${handle}` });
12130
- const node = data.products.nodes[0];
12131
- return node && node.handle === handle ? node.id : null;
12132
- }
12133
- async function shopifyImport(storeDir, options = {}) {
12134
- const auth = requireActive();
12135
- const dir2 = resolve3(storeDir);
12136
- const catalog = readJson(join4(dir2, "catalog.json"), "catalog.json");
12137
- const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
12138
- const locationId = locations.nodes[0]?.id;
12139
- console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
12140
- const productIds = [];
12141
- for (const p of catalog.products) {
12142
- const options_ = p.options ?? [];
12143
- const optionValues = options_.map((name, idx) => ({
12144
- name,
12145
- position: idx + 1,
12146
- values: [...new Set(p.variants.map((v) => v.options[idx]))].map((v) => ({ name: v }))
12147
- }));
12148
- const input = {
12149
- handle: p.handle,
12150
- title: p.title,
12151
- descriptionHtml: p.descriptionHtml ?? "",
12152
- vendor: p.vendor ?? catalog.vendor,
12153
- productType: p.type ?? "",
12154
- tags: p.tags ?? [],
12155
- status: "ACTIVE",
12156
- productOptions: optionValues.length ? optionValues : [{ name: "Title", position: 1, values: [{ name: "Default Title" }] }],
12157
- variants: p.variants.map((v) => ({
12158
- optionValues: options_.length ? options_.map((name, idx) => ({ optionName: name, name: v.options[idx] })) : [{ optionName: "Title", name: "Default Title" }],
12159
- price: v.price,
12160
- compareAtPrice: v.compareAtPrice ?? null,
12161
- inventoryItem: {
12162
- sku: v.sku ?? "",
12163
- tracked: true,
12164
- measurement: { weight: { value: v.grams ?? 0, unit: "GRAMS" } }
12165
- },
12166
- inventoryQuantities: locationId ? [{ locationId, name: "available", quantity: v.inventory ?? 25 }] : []
12167
- }))
12168
- };
12169
- const existingId = await findProductId(auth, p.handle);
12170
- if (existingId)
12171
- input.id = existingId;
12172
- const data = await gql(auth, PRODUCT_SET, { input });
12173
- const errs = data.productSet.userErrors;
12174
- if (errs.length) {
12175
- console.error(` ✗ ${p.handle}: ${joinErrors(errs)}`);
12176
- } else if (data.productSet.product) {
12177
- productIds.push(data.productSet.product.id);
12178
- console.log(` ✓ ${existingId ? "updated" : "created"} ${p.handle} (${p.variants.length} variants)`);
12179
- }
12180
- }
12181
- if (options.prune) {
12182
- const wanted = new Set(catalog.products.map((p) => p.handle));
12183
- const data = await gql(auth, `query($q: String!) { products(first: 100, query: $q) { nodes { id handle } } }`, { q: `vendor:${catalog.vendor}` });
12184
- for (const node of data.products.nodes) {
12185
- if (wanted.has(node.handle))
12186
- continue;
12187
- const del = await gql(auth, `mutation($input: ProductDeleteInput!) {
12188
- productDelete(input: $input) { deletedProductId userErrors { field message } }
12189
- }`, { input: { id: node.id } });
12190
- const errs = del.productDelete.userErrors;
12191
- if (errs.length)
12192
- console.error(` ✗ prune ${node.handle}: ${joinErrors(errs)}`);
12193
- else
12194
- console.log(` ✓ pruned ${node.handle}`);
12195
- }
12196
- }
12197
- for (const c of catalog.collections ?? []) {
12198
- const existing = await gql(auth, `query($q: String!) { collections(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${c.handle}` });
12199
- if (existing.collections.nodes[0]?.handle === c.handle) {
12200
- console.log(` - collection ${c.handle} exists, skipped`);
12201
- continue;
12202
- }
12203
- const data = await gql(auth, COLLECTION_CREATE, {
12204
- input: {
12205
- handle: c.handle,
12206
- title: c.title,
12207
- descriptionHtml: c.descriptionHtml ?? "",
12208
- ruleSet: {
12209
- appliedDisjunctively: false,
12210
- rules: [
12211
- {
12212
- column: "TAG",
12213
- relation: "EQUALS",
12214
- condition: c.rule?.tag ?? `collection:${c.handle}`
12215
- }
12216
- ]
12217
- }
12218
- }
12219
- });
12220
- const errs = data.collectionCreate.userErrors;
12221
- if (errs.length)
12222
- console.error(` ✗ collection ${c.handle}: ${joinErrors(errs)}`);
12223
- else
12224
- console.log(` ✓ collection ${c.handle}`);
12225
- }
12226
- try {
12227
- const pubs = await gql(auth, `{ publications(first: 10) { nodes { id name } } }`);
12228
- const online = pubs.publications.nodes.find((n) => n.name === "Online Store");
12229
- if (online) {
12230
- for (const id of productIds) {
12231
- await gql(auth, `mutation($id: ID!, $input: [PublicationInput!]!) {
12232
- publishablePublish(id: $id, input: $input) { userErrors { field message } }
12233
- }`, { id, input: [{ publicationId: online.id }] });
12234
- }
12235
- console.log(` ✓ published ${productIds.length} products to Online Store`);
12236
- }
12237
- } catch (e) {
12238
- console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
12239
- }
12240
- console.log("Done.");
12241
- }
12242
- async function shopifyUploadImages(storeDir) {
12243
- const auth = requireActive();
12244
- const dir2 = resolve3(storeDir);
12245
- const manifest = readJson(join4(dir2, "images", "manifest.json"), "images/manifest.json");
12246
- console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
12247
- for (const entry of manifest.images) {
12248
- const filePath = join4(dir2, "images", entry.file);
12249
- if (!existsSync4(filePath)) {
12250
- console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
12251
- continue;
12252
- }
12253
- const found = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle
12254
- media(first: 50) { nodes { alt ... on MediaImage { image { url } } } } } } }`, { q: `handle:${entry.handle}` });
12255
- const product = found.products.nodes[0];
12256
- if (!product || product.handle !== entry.handle) {
12257
- console.error(` ✗ ${entry.handle}: product not found in store`);
12258
- continue;
12259
- }
12260
- const stem = basename(entry.file, ".png");
12261
- const already = product.media.nodes.some((m) => m.image?.url?.includes(stem) || entry.alt && m.alt === entry.alt);
12262
- if (already) {
12263
- console.log(` - ${entry.file}: already attached, skipped`);
12264
- continue;
12265
- }
12266
- const bytes = readFileSync4(filePath);
12267
- const staged = await gql(auth, `mutation($input: [StagedUploadInput!]!) {
12268
- stagedUploadsCreate(input: $input) {
12269
- stagedTargets { url resourceUrl parameters { name value } }
12270
- userErrors { field message }
12271
- }
12272
- }`, {
12273
- input: [
12274
- {
12275
- resource: "IMAGE",
12276
- filename: basename(entry.file),
12277
- mimeType: "image/png",
12278
- httpMethod: "PUT",
12279
- fileSize: String(bytes.length)
12280
- }
12281
- ]
12282
- });
12283
- const target = staged.stagedUploadsCreate.stagedTargets?.[0];
12284
- if (!target) {
12285
- console.error(` ✗ ${entry.handle}: staged upload failed: ${JSON.stringify(staged.stagedUploadsCreate.userErrors)}`);
12286
- continue;
12287
- }
12288
- const headers = { "Content-Type": "image/png" };
12289
- for (const p of target.parameters)
12290
- headers[p.name] = p.value;
12291
- const up = await fetch(target.url, {
12292
- method: "PUT",
12293
- headers,
12294
- body: new Uint8Array(bytes)
12295
- });
12296
- if (!up.ok) {
12297
- console.error(` ✗ ${entry.handle}: upload PUT failed (${up.status})`);
12298
- continue;
12299
- }
12300
- const media = await gql(auth, `mutation($productId: ID!, $media: [CreateMediaInput!]!) {
12301
- productCreateMedia(productId: $productId, media: $media) {
12302
- media { alt }
12303
- mediaUserErrors { field message }
12304
- }
12305
- }`, {
12306
- productId: product.id,
12307
- media: [
12308
- { originalSource: target.resourceUrl, alt: entry.alt ?? "", mediaContentType: "IMAGE" }
12309
- ]
12310
- });
12311
- const errs = media.productCreateMedia.mediaUserErrors;
12312
- if (errs.length)
12313
- console.error(` ✗ ${entry.handle}: ${joinErrors(errs)}`);
12314
- else
12315
- console.log(` ✓ ${entry.file}`);
12316
- }
12317
- console.log("Done.");
12318
- }
12064
+ import { readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
12065
+ import { basename as basename3, join as join6, resolve as resolve5 } from "node:path";
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";
12070
+ import { basename as basename2, join as join5, resolve as resolve4 } from "node:path";
12319
12071
 
12320
12072
  // src/theme.ts
12321
12073
  import { spawnSync as spawnSync2 } from "node:child_process";
12322
12074
  import { randomBytes as randomBytes2 } from "node:crypto";
12323
12075
  import {
12324
12076
  cpSync,
12325
- existsSync as existsSync5,
12077
+ existsSync as existsSync4,
12326
12078
  mkdirSync as mkdirSync2,
12327
12079
  mkdtempSync,
12328
12080
  readdirSync as readdirSync2,
12329
- readFileSync as readFileSync5,
12081
+ readFileSync as readFileSync4,
12330
12082
  renameSync,
12331
12083
  rmSync as rmSync3,
12332
12084
  statSync,
@@ -12334,7 +12086,7 @@ import {
12334
12086
  writeFileSync as writeFileSync5
12335
12087
  } from "node:fs";
12336
12088
  import { tmpdir } from "node:os";
12337
- import { basename as basename2, dirname, join as join5, resolve as resolve4 } from "node:path";
12089
+ import { basename, dirname, join as join4, resolve as resolve3 } from "node:path";
12338
12090
  var TEMPLATES_PKG = "@zalify/theme-templates";
12339
12091
  var REGISTRY = "https://registry.npmjs.org";
12340
12092
  var MANIFEST_PATH = ".zalify/theme.json";
@@ -12360,27 +12112,27 @@ async function downloadTarball(url) {
12360
12112
  const res = await fetch(url);
12361
12113
  if (!res.ok)
12362
12114
  throw new Error(`tarball download ${res.status}: ${url}`);
12363
- const file2 = join5(mkdtempSync(join5(tmpdir(), "zalify-theme-")), "pkg.tgz");
12115
+ const file2 = join4(mkdtempSync(join4(tmpdir(), "zalify-theme-")), "pkg.tgz");
12364
12116
  writeFileSync5(file2, Buffer.from(await res.arrayBuffer()));
12365
12117
  return file2;
12366
12118
  }
12367
12119
  function extractTarball(file2) {
12368
- const dir2 = mkdtempSync(join5(tmpdir(), "zalify-theme-x-"));
12369
- const result = spawnSync2("tar", ["-xzf", resolve4(file2), "-C", dir2], {
12120
+ const dir2 = mkdtempSync(join4(tmpdir(), "zalify-theme-x-"));
12121
+ const result = spawnSync2("tar", ["-xzf", resolve3(file2), "-C", dir2], {
12370
12122
  encoding: "utf8"
12371
12123
  });
12372
12124
  if (result.error || result.status !== 0) {
12373
12125
  throw new Error(`tar extract failed for ${file2}: ${result.stderr || result.error?.message}`);
12374
12126
  }
12375
- const root = join5(dir2, "package");
12376
- if (!existsSync5(root))
12127
+ const root = join4(dir2, "package");
12128
+ if (!existsSync4(root))
12377
12129
  throw new Error(`unexpected tarball layout in ${file2} (no package/)`);
12378
12130
  return root;
12379
12131
  }
12380
12132
  async function acquireTemplatePkg(localTarball, version) {
12381
12133
  if (localTarball) {
12382
12134
  const root = extractTarball(localTarball);
12383
- const v = JSON.parse(readFileSync5(join5(root, "package.json"), "utf8")).version;
12135
+ const v = JSON.parse(readFileSync4(join4(root, "package.json"), "utf8")).version;
12384
12136
  return { root, version: v };
12385
12137
  }
12386
12138
  const packument = await fetchPackument();
@@ -12391,11 +12143,11 @@ async function acquireTemplatePkg(localTarball, version) {
12391
12143
  return { root: extractTarball(await downloadTarball(entry.dist.tarball)), version: target };
12392
12144
  }
12393
12145
  function readTemplateManifest(pkgRoot, template) {
12394
- const path9 = join5(pkgRoot, "templates", template, MANIFEST_PATH);
12395
- if (!existsSync5(path9)) {
12146
+ const path9 = join4(pkgRoot, "templates", template, MANIFEST_PATH);
12147
+ if (!existsSync4(path9)) {
12396
12148
  throw new Error(`template "${template}" has no ${MANIFEST_PATH} — this ${TEMPLATES_PKG} version predates the upgrade contract`);
12397
12149
  }
12398
- return JSON.parse(readFileSync5(path9, "utf8"));
12150
+ return JSON.parse(readFileSync4(path9, "utf8"));
12399
12151
  }
12400
12152
  function resolveVariant(manifest, variant) {
12401
12153
  const resolved = { ...manifest, files: { ...manifest.files }, variant };
@@ -12409,48 +12161,48 @@ function templateFilePath(templateDir, rel, variant) {
12409
12161
  const stashed = rel.replace(/(^|\/)\.gitignore$/, "$1gitignore");
12410
12162
  if (variant === "editor") {
12411
12163
  for (const candidate of [rel, stashed]) {
12412
- const overlay = join5(templateDir, "__editor__", candidate);
12413
- if (existsSync5(overlay))
12164
+ const overlay = join4(templateDir, "__editor__", candidate);
12165
+ if (existsSync4(overlay))
12414
12166
  return overlay;
12415
12167
  }
12416
12168
  }
12417
12169
  for (const candidate of [rel, stashed]) {
12418
- const path9 = join5(templateDir, candidate);
12419
- if (existsSync5(path9))
12170
+ const path9 = join4(templateDir, candidate);
12171
+ if (existsSync4(path9))
12420
12172
  return path9;
12421
12173
  }
12422
12174
  throw new Error(`file listed in manifest but missing from template: ${rel}`);
12423
12175
  }
12424
12176
  function readProjectManifest(dir2) {
12425
- const path9 = join5(dir2, MANIFEST_PATH);
12426
- if (!existsSync5(path9)) {
12177
+ const path9 = join4(dir2, MANIFEST_PATH);
12178
+ if (!existsSync4(path9)) {
12427
12179
  throw new Error(`no ${MANIFEST_PATH} in ${dir2} — not a scaffolded Zalify storefront.
12428
12180
  ` + ` Scaffold one with \`zalify theme create\`.`);
12429
12181
  }
12430
- return JSON.parse(readFileSync5(path9, "utf8"));
12182
+ return JSON.parse(readFileSync4(path9, "utf8"));
12431
12183
  }
12432
12184
  function writeJson(path9, value) {
12433
12185
  writeFileSync5(path9, JSON.stringify(value, null, 2) + `
12434
12186
  `);
12435
12187
  }
12436
12188
  async function themeCreate(dir2, opts) {
12437
- const targetDir = resolve4(dir2);
12438
- if (existsSync5(targetDir) && readdirSync2(targetDir).length > 0) {
12189
+ const targetDir = resolve3(dir2);
12190
+ if (existsSync4(targetDir) && readdirSync2(targetDir).length > 0) {
12439
12191
  throw new Error(`${dir2} already exists and is not empty`);
12440
12192
  }
12441
12193
  const { root, version } = await acquireTemplatePkg(opts.tarball, opts.to);
12442
- const frameworks = JSON.parse(readFileSync5(join5(root, "templates.json"), "utf8"));
12194
+ const frameworks = JSON.parse(readFileSync4(join4(root, "templates.json"), "utf8"));
12443
12195
  const template = opts.template ?? "nextjs";
12444
12196
  const meta = frameworks.frameworks[template];
12445
12197
  if (!meta) {
12446
12198
  throw new Error(`unknown template "${template}" — expected one of: ${Object.keys(frameworks.frameworks).join(", ")}`);
12447
12199
  }
12448
- const templateDir = join5(root, "templates", template);
12200
+ const templateDir = join4(root, "templates", template);
12449
12201
  console.log(`Scaffolding ${meta.label} (${TEMPLATES_PKG}@${version}) into ${dir2}`);
12450
12202
  mkdirSync2(targetDir, { recursive: true });
12451
12203
  cpSync(templateDir, targetDir, { recursive: true });
12452
- const overlayDir = join5(targetDir, "__editor__");
12453
- const useEditor = Boolean(opts.editor) && existsSync5(overlayDir);
12204
+ const overlayDir = join4(targetDir, "__editor__");
12205
+ const useEditor = Boolean(opts.editor) && existsSync4(overlayDir);
12454
12206
  if (opts.editor && !useEditor) {
12455
12207
  console.log(` ! --editor has no effect for ${template} — no editor overlay in this template`);
12456
12208
  }
@@ -12459,16 +12211,16 @@ async function themeCreate(dir2, opts) {
12459
12211
  rmSync3(overlayDir, { recursive: true, force: true });
12460
12212
  (function restoreGitignores(d) {
12461
12213
  for (const entry of readdirSync2(d)) {
12462
- const full = join5(d, entry);
12214
+ const full = join4(d, entry);
12463
12215
  if (statSync(full).isDirectory())
12464
12216
  restoreGitignores(full);
12465
12217
  else if (entry === "gitignore")
12466
- renameSync(full, join5(d, ".gitignore"));
12218
+ renameSync(full, join4(d, ".gitignore"));
12467
12219
  }
12468
12220
  })(targetDir);
12469
- const pkgPath = join5(targetDir, "package.json");
12470
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
12471
- pkg.name = basename2(targetDir).toLowerCase().replaceAll(/[^a-z0-9-_.]+/g, "-").replaceAll(/^[-.]+|[-.]+$/g, "") || "zalify-storefront";
12221
+ const pkgPath = join4(targetDir, "package.json");
12222
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
12223
+ pkg.name = basename(targetDir).toLowerCase().replaceAll(/[^a-z0-9-_.]+/g, "-").replaceAll(/^[-.]+|[-.]+$/g, "") || "zalify-storefront";
12472
12224
  writeJson(pkgPath, pkg);
12473
12225
  if (meta.env) {
12474
12226
  const lines = [
@@ -12480,12 +12232,12 @@ async function themeCreate(dir2, opts) {
12480
12232
  if (meta.env.sessionSecret) {
12481
12233
  lines.push("# Session cookie signing key — unique to this project.", `${meta.env.sessionSecret}=${randomBytes2(32).toString("hex")}`);
12482
12234
  }
12483
- writeFileSync5(join5(targetDir, ".env"), lines.join(`
12235
+ writeFileSync5(join4(targetDir, ".env"), lines.join(`
12484
12236
  `) + `
12485
12237
  `);
12486
12238
  }
12487
12239
  const manifest = readProjectManifest(targetDir);
12488
- writeJson(join5(targetDir, MANIFEST_PATH), resolveVariant(manifest, useEditor ? "editor" : "default"));
12240
+ writeJson(join4(targetDir, MANIFEST_PATH), resolveVariant(manifest, useEditor ? "editor" : "default"));
12489
12241
  if (opts.git !== false) {
12490
12242
  const git = (args) => spawnSync2("git", args, { cwd: targetDir, stdio: "ignore" });
12491
12243
  if (git(["init", "-b", "main"]).status === 0) {
@@ -12518,17 +12270,17 @@ Runs on mock.shop demo data out of the box — edit .env to connect your store.`
12518
12270
  console.log("Your theme customizations live in theme/ — see theme/README.md.");
12519
12271
  }
12520
12272
  function themeStatus(dir2 = ".") {
12521
- const projectDir = resolve4(dir2);
12273
+ const projectDir = resolve3(dir2);
12522
12274
  const manifest = readProjectManifest(projectDir);
12523
12275
  console.log(`${manifest.template} theme, ${TEMPLATES_PKG}@${manifest.version}` + (manifest.variant === "editor" ? " (editor variant)" : ""));
12524
12276
  let clean = 0;
12525
12277
  const edited = [];
12526
12278
  const missing = [];
12527
12279
  for (const [rel, expected] of Object.entries(manifest.files)) {
12528
- const full = join5(projectDir, rel);
12529
- if (!existsSync5(full))
12280
+ const full = join4(projectDir, rel);
12281
+ if (!existsSync4(full))
12530
12282
  missing.push(rel);
12531
- else if (sha256(readFileSync5(full)) !== expected)
12283
+ else if (sha256(readFileSync4(full)) !== expected)
12532
12284
  edited.push(rel);
12533
12285
  else
12534
12286
  clean++;
@@ -12546,7 +12298,7 @@ function looksBinary(...buffers) {
12546
12298
  return buffers.some((b) => b.subarray(0, 8000).includes(0));
12547
12299
  }
12548
12300
  async function themeUpgrade(dir2 = ".", opts = {}) {
12549
- const projectDir = resolve4(dir2);
12301
+ const projectDir = resolve3(dir2);
12550
12302
  const local = readProjectManifest(projectDir);
12551
12303
  const variant = local.variant ?? "default";
12552
12304
  const target = await acquireTemplatePkg(opts.tarball, opts.to);
@@ -12560,8 +12312,8 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12560
12312
  }
12561
12313
  const oldManifest = resolveVariant(readTemplateManifest(base.root, local.template), variant);
12562
12314
  const newManifest = resolveVariant(readTemplateManifest(target.root, local.template), variant);
12563
- const oldDir = join5(base.root, "templates", local.template);
12564
- const newDir = join5(target.root, "templates", local.template);
12315
+ const oldDir = join4(base.root, "templates", local.template);
12316
+ const newDir = join4(target.root, "templates", local.template);
12565
12317
  console.log(`Upgrading ${local.template} theme ${local.version} → ${target.version}` + (opts.dryRun ? " (dry run)" : ""));
12566
12318
  const counts = {
12567
12319
  overwrite: 0,
@@ -12579,9 +12331,9 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12579
12331
  continue;
12580
12332
  const oldHash = oldManifest.files[rel];
12581
12333
  const newHash = newManifest.files[rel];
12582
- const localPath = join5(projectDir, rel);
12583
- const localExists = existsSync5(localPath);
12584
- const localBytes = localExists ? readFileSync5(localPath) : null;
12334
+ const localPath = join4(projectDir, rel);
12335
+ const localExists = existsSync4(localPath);
12336
+ const localBytes = localExists ? readFileSync4(localPath) : null;
12585
12337
  const localHash = localBytes ? sha256(localBytes) : null;
12586
12338
  const write = (bytes) => {
12587
12339
  if (opts.dryRun)
@@ -12595,7 +12347,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12595
12347
  console.log(` ! missing ${rel} (theme file was deleted locally; unchanged upstream — leaving as is)`);
12596
12348
  continue;
12597
12349
  }
12598
- const newBytes = readFileSync5(templateFilePath(newDir, rel, variant));
12350
+ const newBytes = readFileSync4(templateFilePath(newDir, rel, variant));
12599
12351
  if (!localExists) {
12600
12352
  write(newBytes);
12601
12353
  counts.add++;
@@ -12605,7 +12357,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12605
12357
  counts.overwrite++;
12606
12358
  console.log(` ✓ ${opts.dryRun ? "would update" : "updated"} ${rel}`);
12607
12359
  } else if (localHash === newHash) {} else {
12608
- const oldBytes = readFileSync5(templateFilePath(oldDir, rel, variant));
12360
+ const oldBytes = readFileSync4(templateFilePath(oldDir, rel, variant));
12609
12361
  if (looksBinary(localBytes, oldBytes, newBytes)) {
12610
12362
  if (!opts.dryRun)
12611
12363
  writeFileSync5(`${localPath}.new`, newBytes);
@@ -12619,9 +12371,9 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12619
12371
  console.log(` ~ would merge ${rel}`);
12620
12372
  continue;
12621
12373
  }
12622
- const scratch = mkdtempSync(join5(tmpdir(), "zalify-merge-"));
12623
- const basePath = join5(scratch, "base");
12624
- const newPath = join5(scratch, "new");
12374
+ const scratch = mkdtempSync(join4(tmpdir(), "zalify-merge-"));
12375
+ const basePath = join4(scratch, "base");
12376
+ const newPath = join4(scratch, "new");
12625
12377
  writeFileSync5(basePath, oldBytes);
12626
12378
  writeFileSync5(newPath, newBytes);
12627
12379
  const result = spawnSync2("git", ["merge-file", "-L", "current", "-L", `base (${base.version})`, "-L", `new (${target.version})`, localPath, basePath, newPath], { encoding: "utf8" });
@@ -12654,7 +12406,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12654
12406
  }
12655
12407
  } else if (!oldHash && newHash) {
12656
12408
  if (!localExists) {
12657
- write(readFileSync5(templateFilePath(newDir, rel, variant)));
12409
+ write(readFileSync4(templateFilePath(newDir, rel, variant)));
12658
12410
  counts.add++;
12659
12411
  console.log(` + ${opts.dryRun ? "would add" : "added"} ${rel}`);
12660
12412
  } else if (localHash !== newHash) {
@@ -12665,7 +12417,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
12665
12417
  }
12666
12418
  const depsChanged = mergePackageJson(projectDir, oldDir, newDir, opts.dryRun ?? false);
12667
12419
  if (!opts.dryRun) {
12668
- writeJson(join5(projectDir, MANIFEST_PATH), { ...newManifest, version: target.version });
12420
+ writeJson(join4(projectDir, MANIFEST_PATH), { ...newManifest, version: target.version });
12669
12421
  }
12670
12422
  const summary = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k.replace("-", " ")}`).join(", ");
12671
12423
  console.log(`
@@ -12686,7 +12438,7 @@ ${opts.dryRun ? "Would apply" : "Done"}: ${summary || "no file changes"}.`);
12686
12438
  }
12687
12439
  }
12688
12440
  function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
12689
- const read = (p) => JSON.parse(readFileSync5(join5(p, "package.json"), "utf8"));
12441
+ const read = (p) => JSON.parse(readFileSync4(join4(p, "package.json"), "utf8"));
12690
12442
  const localPkg = read(projectDir);
12691
12443
  const oldPkg = read(oldDir);
12692
12444
  const newPkg = read(newDir);
@@ -12735,20 +12487,435 @@ function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
12735
12487
  console.log(` ✓ package.json ${key} updated`);
12736
12488
  }
12737
12489
  if (changed && !dryRun) {
12738
- writeFileSync5(join5(projectDir, "package.json"), JSON.stringify(localPkg, null, 2) + `
12490
+ writeFileSync5(join4(projectDir, "package.json"), JSON.stringify(localPkg, null, 2) + `
12739
12491
  `);
12740
12492
  }
12741
12493
  return depsChanged;
12742
12494
  }
12743
12495
 
12744
- // src/cli.ts
12745
- var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
12746
- var require2 = createRequire2(import.meta.url);
12747
- var pkg = require2(join6(__dirname4, "..", "package.json"));
12748
- try {
12749
- updateNotifier({ pkg }).notify({
12750
- isGlobal: true,
12751
- message: "Update available {currentVersion} → {latestVersion}\nRun `zalify self-update`"
12496
+ // src/storefront.ts
12497
+ function apiError2(status, json, appUrl) {
12498
+ if (status === 402) {
12499
+ return new Error(`${json.error ?? "Upgrade required"}
12500
+ ` + ` Storefront hosting is a paid feature — upgrade the workspace in ${appUrl}`);
12501
+ }
12502
+ if (json.code === "NO_CONNECTION") {
12503
+ return new Error(`${json.error}
12504
+ Connect the workspace to its Shopify store first, then re-run.`);
12505
+ }
12506
+ if (json.code === "SLUG_TAKEN") {
12507
+ return new Error(`${json.error}
12508
+ Pick a different directory/slug name.`);
12509
+ }
12510
+ if (status === 404) {
12511
+ return new Error("The storefronts API is not deployed yet (404). Update app.zalify.com first.");
12512
+ }
12513
+ if (status === 503) {
12514
+ return new Error(json.error ?? "Storefront provisioning is not configured on the server.");
12515
+ }
12516
+ return new Error(`storefronts API ${status}: ${JSON.stringify(json)}`);
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
+ });
12524
+ const json = await res.json().catch(() => ({}));
12525
+ if (!res.ok)
12526
+ throw apiError2(res.status, json, auth.appUrl);
12527
+ return json;
12528
+ }
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;
12538
+ }
12539
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
12540
+ async function pollUntilReady(auth, id) {
12541
+ let lastMessage = "";
12542
+ const deadline = Date.now() + 10 * 60000;
12543
+ while (Date.now() < deadline) {
12544
+ const view = await get(auth, `/api/storefronts/${id}`);
12545
+ const message = view.statusMessage ?? view.status;
12546
+ if (message !== lastMessage) {
12547
+ console.log(` … ${message}`);
12548
+ lastMessage = message;
12549
+ }
12550
+ if (view.status === "READY")
12551
+ return view;
12552
+ if (view.status === "FAILED") {
12553
+ throw new Error(`Provisioning failed: ${view.statusMessage ?? "unknown reason"}`);
12554
+ }
12555
+ await sleep2(3000);
12556
+ }
12557
+ throw new Error("Provisioning timed out after 10 minutes — check the Temporal UI.");
12558
+ }
12559
+ async function storefrontCreate(dir2, opts) {
12560
+ 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", {
12564
+ slug,
12565
+ theme: opts.theme ?? "nextjs",
12566
+ ...opts.to ? { themeVersion: opts.to } : {}
12567
+ });
12568
+ const view = await pollUntilReady(auth, storefrontId);
12569
+ console.log(` ✓ provisioned: ${view.githubRepo} → https://${view.domain}`);
12570
+ await themeCreate(dir2, {
12571
+ template: view.theme,
12572
+ editor: opts.editor,
12573
+ storeDomain: view.storeDomain ?? "",
12574
+ storefrontToken: view.storefrontToken ?? "",
12575
+ to: opts.to,
12576
+ install: opts.install,
12577
+ git: opts.git
12578
+ });
12579
+ if (opts.git !== false && view.githubRepo) {
12580
+ const { token, repo } = await post(auth, `/api/storefronts/${storefrontId}/push-token`, {});
12581
+ const run = (args) => spawnSync3("git", args, { cwd: resolve4(dir2), stdio: "ignore" });
12582
+ run(["remote", "add", "origin", `https://github.com/${repo}.git`]);
12583
+ const push = spawnSync3("git", ["push", "-u", `https://x-access-token:${token}@github.com/${repo}.git`, "main"], { cwd: resolve4(dir2), stdio: "inherit" });
12584
+ if (push.status !== 0) {
12585
+ throw new Error(`git push to ${repo} failed — resolve and push manually.`);
12586
+ }
12587
+ run(["branch", "--set-upstream-to=origin/main", "main"]);
12588
+ console.log(` ✓ pushed to github.com/${repo} — Vercel deploys on push`);
12589
+ }
12590
+ if (view.domain) {
12591
+ console.log(` Waiting for https://${view.domain} …`);
12592
+ const deadline = Date.now() + 5 * 60000;
12593
+ while (Date.now() < deadline) {
12594
+ try {
12595
+ const res = await fetch(`https://${view.domain}/`, { redirect: "manual" });
12596
+ if (res.ok) {
12597
+ console.log(`
12598
+ Live: https://${view.domain}`);
12599
+ break;
12600
+ }
12601
+ } catch {}
12602
+ await sleep2(1e4);
12603
+ }
12604
+ }
12605
+ console.log(`
12606
+ Next steps:
12607
+ ` + ` - author your theme in ${dir2}/theme/ (see theme/README.md), commit + push
12608
+ ` + ` - zalify shopify import stores/<slug> --prune # catalog (auto-flushes the site cache)
12609
+ ` + ` - zalify theme status ${dir2} # drift check anytime`);
12610
+ }
12611
+ async function storefrontList() {
12612
+ const auth = requireActive();
12613
+ const { storefronts } = await get(auth, "/api/storefronts");
12614
+ if (!storefronts.length) {
12615
+ console.log(`No storefronts in workspace "${auth.workspaceName}".`);
12616
+ return;
12617
+ }
12618
+ for (const s of storefronts) {
12619
+ const site = s.domain ? `https://${s.domain}` : "-";
12620
+ console.log(`${s.status === "READY" ? "✓" : s.status === "FAILED" ? "✗" : "…"} ${s.slug} ${site} (${s.status.toLowerCase()}${s.githubRepo ? `, ${s.githubRepo}` : ""})`);
12621
+ }
12622
+ }
12623
+ function resolveSlug(slugOrDir) {
12624
+ const storeJson = join5(resolve4(slugOrDir), "store.json");
12625
+ if (existsSync5(storeJson)) {
12626
+ const parsed = JSON.parse(readFileSync5(storeJson, "utf8"));
12627
+ if (parsed.slug)
12628
+ return parsed.slug;
12629
+ }
12630
+ return basename2(slugOrDir);
12631
+ }
12632
+ async function storefrontRevalidate(slugOrDir) {
12633
+ const auth = requireActive();
12634
+ const slug = resolveSlug(slugOrDir);
12635
+ const { storefronts } = await get(auth, "/api/storefronts");
12636
+ const match = storefronts.find((s) => s.slug === slug);
12637
+ if (!match)
12638
+ throw new Error(`No storefront "${slug}" in workspace "${auth.workspaceName}".`);
12639
+ await post(auth, `/api/storefronts/${match.id}/revalidate`, {});
12640
+ console.log(`✓ flushed cache on https://${match.domain}`);
12641
+ }
12642
+ async function maybeRevalidateStorefront(auth, storeDir) {
12643
+ try {
12644
+ const slug = resolveSlug(storeDir);
12645
+ const { storefronts } = await get(auth, "/api/storefronts");
12646
+ const match = storefronts.find((s) => s.slug === slug && s.status === "READY");
12647
+ if (!match)
12648
+ return;
12649
+ await post(auth, `/api/storefronts/${match.id}/revalidate`, {});
12650
+ console.log(`✓ site cache flushed (https://${match.domain})`);
12651
+ } catch {}
12652
+ }
12653
+
12654
+ // src/shopify.ts
12655
+ function proxyError(auth, status, json) {
12656
+ if (json.code === "SCOPES_MISSING") {
12657
+ const granted = new Set(json.grantedScopes ?? []);
12658
+ const missing = (json.requiredScopes ?? []).filter((s) => !granted.has(s));
12659
+ return new Error(`Your Shopify connection is missing newly added permissions: ${missing.join(", ") || "(unknown)"}.
12660
+ ` + ` Reconnect the store (one-time approval): ${json.reconnectUrl ?? `${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`}
12661
+ ` + ` Then re-run this command.`);
12662
+ }
12663
+ if (status === 404) {
12664
+ return new Error("Shopify Admin proxy not found (404) — the /api/shopify/admin endpoint " + "is probably not deployed yet on this Zalify app. Try again after the next deploy.");
12665
+ }
12666
+ if (status === 402 || json.code === "UPGRADE_REQUIRED") {
12667
+ return new Error(`${json.error ?? "Paid plan required."}
12668
+ Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
12669
+ }
12670
+ if (status === 409) {
12671
+ return new Error(`${json.error ?? "No Shopify store is connected to this workspace."}
12672
+ ` + ` Connect one: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`);
12673
+ }
12674
+ return new Error(`shopify/admin ${status}: ${JSON.stringify(json)}`);
12675
+ }
12676
+ async function gql(auth, query, variables = {}) {
12677
+ const res = await fetch(`${auth.appUrl}/api/shopify/admin`, {
12678
+ method: "POST",
12679
+ headers: {
12680
+ "Content-Type": "application/json",
12681
+ Authorization: `Bearer ${auth.key}`
12682
+ },
12683
+ body: JSON.stringify({ workspaceId: auth.workspaceId, query, variables })
12684
+ });
12685
+ const json = await res.json().catch(() => ({}));
12686
+ if (!res.ok)
12687
+ throw proxyError(auth, res.status, json);
12688
+ if (json.errors)
12689
+ throw new Error(JSON.stringify(json.errors));
12690
+ if (!json.data)
12691
+ throw new Error(`shopify/admin: empty response`);
12692
+ return json.data;
12693
+ }
12694
+ function readJson(path9, label) {
12695
+ if (!existsSync6(path9)) {
12696
+ throw new Error(`No ${label} at ${path9}`);
12697
+ }
12698
+ return JSON.parse(readFileSync6(path9, "utf8"));
12699
+ }
12700
+ function joinErrors(errs) {
12701
+ return errs.map((e) => e.message).join("; ");
12702
+ }
12703
+ var PRODUCT_SET = `
12704
+ mutation productSet($input: ProductSetInput!) {
12705
+ productSet(input: $input, synchronous: true) {
12706
+ product { id handle }
12707
+ userErrors { field message }
12708
+ }
12709
+ }`;
12710
+ var COLLECTION_CREATE = `
12711
+ mutation collectionCreate($input: CollectionInput!) {
12712
+ collectionCreate(input: $input) {
12713
+ collection { id handle }
12714
+ userErrors { field message }
12715
+ }
12716
+ }`;
12717
+ async function findProductId(auth, handle) {
12718
+ const data = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${handle}` });
12719
+ const node = data.products.nodes[0];
12720
+ return node && node.handle === handle ? node.id : null;
12721
+ }
12722
+ async function shopifyImport(storeDir, options = {}) {
12723
+ const auth = requireActive();
12724
+ const dir2 = resolve5(storeDir);
12725
+ const catalog = readJson(join6(dir2, "catalog.json"), "catalog.json");
12726
+ const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
12727
+ const locationId = locations.nodes[0]?.id;
12728
+ console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
12729
+ const productIds = [];
12730
+ for (const p of catalog.products) {
12731
+ const options_ = p.options ?? [];
12732
+ const optionValues = options_.map((name, idx) => ({
12733
+ name,
12734
+ position: idx + 1,
12735
+ values: [...new Set(p.variants.map((v) => v.options[idx]))].map((v) => ({ name: v }))
12736
+ }));
12737
+ const input = {
12738
+ handle: p.handle,
12739
+ title: p.title,
12740
+ descriptionHtml: p.descriptionHtml ?? "",
12741
+ vendor: p.vendor ?? catalog.vendor,
12742
+ productType: p.type ?? "",
12743
+ tags: p.tags ?? [],
12744
+ status: "ACTIVE",
12745
+ productOptions: optionValues.length ? optionValues : [{ name: "Title", position: 1, values: [{ name: "Default Title" }] }],
12746
+ variants: p.variants.map((v) => ({
12747
+ optionValues: options_.length ? options_.map((name, idx) => ({ optionName: name, name: v.options[idx] })) : [{ optionName: "Title", name: "Default Title" }],
12748
+ price: v.price,
12749
+ compareAtPrice: v.compareAtPrice ?? null,
12750
+ inventoryItem: {
12751
+ sku: v.sku ?? "",
12752
+ tracked: true,
12753
+ measurement: { weight: { value: v.grams ?? 0, unit: "GRAMS" } }
12754
+ },
12755
+ inventoryQuantities: locationId ? [{ locationId, name: "available", quantity: v.inventory ?? 25 }] : []
12756
+ }))
12757
+ };
12758
+ const existingId = await findProductId(auth, p.handle);
12759
+ if (existingId)
12760
+ input.id = existingId;
12761
+ const data = await gql(auth, PRODUCT_SET, { input });
12762
+ const errs = data.productSet.userErrors;
12763
+ if (errs.length) {
12764
+ console.error(` ✗ ${p.handle}: ${joinErrors(errs)}`);
12765
+ } else if (data.productSet.product) {
12766
+ productIds.push(data.productSet.product.id);
12767
+ console.log(` ✓ ${existingId ? "updated" : "created"} ${p.handle} (${p.variants.length} variants)`);
12768
+ }
12769
+ }
12770
+ if (options.prune) {
12771
+ const wanted = new Set(catalog.products.map((p) => p.handle));
12772
+ const data = await gql(auth, `query($q: String!) { products(first: 100, query: $q) { nodes { id handle } } }`, { q: `vendor:${catalog.vendor}` });
12773
+ for (const node of data.products.nodes) {
12774
+ if (wanted.has(node.handle))
12775
+ continue;
12776
+ const del = await gql(auth, `mutation($input: ProductDeleteInput!) {
12777
+ productDelete(input: $input) { deletedProductId userErrors { field message } }
12778
+ }`, { input: { id: node.id } });
12779
+ const errs = del.productDelete.userErrors;
12780
+ if (errs.length)
12781
+ console.error(` ✗ prune ${node.handle}: ${joinErrors(errs)}`);
12782
+ else
12783
+ console.log(` ✓ pruned ${node.handle}`);
12784
+ }
12785
+ }
12786
+ for (const c of catalog.collections ?? []) {
12787
+ const existing = await gql(auth, `query($q: String!) { collections(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${c.handle}` });
12788
+ if (existing.collections.nodes[0]?.handle === c.handle) {
12789
+ console.log(` - collection ${c.handle} exists, skipped`);
12790
+ continue;
12791
+ }
12792
+ const data = await gql(auth, COLLECTION_CREATE, {
12793
+ input: {
12794
+ handle: c.handle,
12795
+ title: c.title,
12796
+ descriptionHtml: c.descriptionHtml ?? "",
12797
+ ruleSet: {
12798
+ appliedDisjunctively: false,
12799
+ rules: [
12800
+ {
12801
+ column: "TAG",
12802
+ relation: "EQUALS",
12803
+ condition: c.rule?.tag ?? `collection:${c.handle}`
12804
+ }
12805
+ ]
12806
+ }
12807
+ }
12808
+ });
12809
+ const errs = data.collectionCreate.userErrors;
12810
+ if (errs.length)
12811
+ console.error(` ✗ collection ${c.handle}: ${joinErrors(errs)}`);
12812
+ else
12813
+ console.log(` ✓ collection ${c.handle}`);
12814
+ }
12815
+ try {
12816
+ const pubs = await gql(auth, `{ publications(first: 10) { nodes { id name } } }`);
12817
+ const online = pubs.publications.nodes.find((n) => n.name === "Online Store");
12818
+ if (online) {
12819
+ for (const id of productIds) {
12820
+ await gql(auth, `mutation($id: ID!, $input: [PublicationInput!]!) {
12821
+ publishablePublish(id: $id, input: $input) { userErrors { field message } }
12822
+ }`, { id, input: [{ publicationId: online.id }] });
12823
+ }
12824
+ console.log(` ✓ published ${productIds.length} products to Online Store`);
12825
+ }
12826
+ } catch (e) {
12827
+ console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
12828
+ }
12829
+ console.log("Done.");
12830
+ await maybeRevalidateStorefront(auth, dir2);
12831
+ }
12832
+ async function shopifyUploadImages(storeDir) {
12833
+ const auth = requireActive();
12834
+ const dir2 = resolve5(storeDir);
12835
+ const manifest = readJson(join6(dir2, "images", "manifest.json"), "images/manifest.json");
12836
+ console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
12837
+ for (const entry of manifest.images) {
12838
+ const filePath = join6(dir2, "images", entry.file);
12839
+ if (!existsSync6(filePath)) {
12840
+ console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
12841
+ continue;
12842
+ }
12843
+ const found = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle
12844
+ media(first: 50) { nodes { alt ... on MediaImage { image { url } } } } } } }`, { q: `handle:${entry.handle}` });
12845
+ const product = found.products.nodes[0];
12846
+ if (!product || product.handle !== entry.handle) {
12847
+ console.error(` ✗ ${entry.handle}: product not found in store`);
12848
+ continue;
12849
+ }
12850
+ const stem = basename3(entry.file, ".png");
12851
+ const already = product.media.nodes.some((m) => m.image?.url?.includes(stem) || entry.alt && m.alt === entry.alt);
12852
+ if (already) {
12853
+ console.log(` - ${entry.file}: already attached, skipped`);
12854
+ continue;
12855
+ }
12856
+ const bytes = readFileSync6(filePath);
12857
+ const staged = await gql(auth, `mutation($input: [StagedUploadInput!]!) {
12858
+ stagedUploadsCreate(input: $input) {
12859
+ stagedTargets { url resourceUrl parameters { name value } }
12860
+ userErrors { field message }
12861
+ }
12862
+ }`, {
12863
+ input: [
12864
+ {
12865
+ resource: "IMAGE",
12866
+ filename: basename3(entry.file),
12867
+ mimeType: "image/png",
12868
+ httpMethod: "PUT",
12869
+ fileSize: String(bytes.length)
12870
+ }
12871
+ ]
12872
+ });
12873
+ const target = staged.stagedUploadsCreate.stagedTargets?.[0];
12874
+ if (!target) {
12875
+ console.error(` ✗ ${entry.handle}: staged upload failed: ${JSON.stringify(staged.stagedUploadsCreate.userErrors)}`);
12876
+ continue;
12877
+ }
12878
+ const headers = { "Content-Type": "image/png" };
12879
+ for (const p of target.parameters)
12880
+ headers[p.name] = p.value;
12881
+ const up = await fetch(target.url, {
12882
+ method: "PUT",
12883
+ headers,
12884
+ body: new Uint8Array(bytes)
12885
+ });
12886
+ if (!up.ok) {
12887
+ console.error(` ✗ ${entry.handle}: upload PUT failed (${up.status})`);
12888
+ continue;
12889
+ }
12890
+ const media = await gql(auth, `mutation($productId: ID!, $media: [CreateMediaInput!]!) {
12891
+ productCreateMedia(productId: $productId, media: $media) {
12892
+ media { alt }
12893
+ mediaUserErrors { field message }
12894
+ }
12895
+ }`, {
12896
+ productId: product.id,
12897
+ media: [
12898
+ { originalSource: target.resourceUrl, alt: entry.alt ?? "", mediaContentType: "IMAGE" }
12899
+ ]
12900
+ });
12901
+ const errs = media.productCreateMedia.mediaUserErrors;
12902
+ if (errs.length)
12903
+ console.error(` ✗ ${entry.handle}: ${joinErrors(errs)}`);
12904
+ else
12905
+ console.log(` ✓ ${entry.file}`);
12906
+ }
12907
+ console.log("Done.");
12908
+ await maybeRevalidateStorefront(auth, dir2);
12909
+ }
12910
+
12911
+ // src/cli.ts
12912
+ var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
12913
+ var require2 = createRequire2(import.meta.url);
12914
+ var pkg = require2(join7(__dirname4, "..", "package.json"));
12915
+ try {
12916
+ updateNotifier({ pkg }).notify({
12917
+ isGlobal: true,
12918
+ message: "Update available {currentVersion} → {latestVersion}\nRun `zalify self-update`"
12752
12919
  });
12753
12920
  } catch {}
12754
12921
  var program2 = new Command;
@@ -12799,6 +12966,16 @@ shopify.command("import <store-dir>").description("Import <store-dir>/catalog.js
12799
12966
  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) => {
12800
12967
  await shopifyUploadImages(storeDir);
12801
12968
  });
12969
+ var storefront = program2.command("storefront").description("Provision and manage fully-hosted standalone storefronts (repo + Vercel via Zalify)");
12970
+ 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) => {
12971
+ await storefrontCreate(dir2, options);
12972
+ });
12973
+ storefront.command("list", { isDefault: true }).description("List the workspace's provisioned storefronts").action(async () => {
12974
+ await storefrontList();
12975
+ });
12976
+ storefront.command("revalidate <slug-or-store-dir>").description("Flush the storefront site's data cache (secret stays server-side)").action(async (slugOrDir) => {
12977
+ await storefrontRevalidate(slugOrDir);
12978
+ });
12802
12979
  var theme = program2.command("theme").description("Scaffold and upgrade standalone Zalify storefronts (Shopify-theme model)");
12803
12980
  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) => {
12804
12981
  await themeCreate(dir2, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Zalify CLI - command-line interface for Zalify",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",