@zalify/cli 0.7.1 → 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 +635 -20
  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, join as join5 } 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
@@ -11600,8 +11600,13 @@ async function whoami() {
11600
11600
  console.log(live ? "status ✓ key verified" : `status ! not verified — ${liveError}`);
11601
11601
  }
11602
11602
 
11603
- // src/assets.ts
11603
+ // src/hash.ts
11604
11604
  import { createHash } from "node:crypto";
11605
+ function sha256(bytes) {
11606
+ return createHash("sha256").update(bytes).digest("hex");
11607
+ }
11608
+
11609
+ // src/assets.ts
11605
11610
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync2, readdirSync } from "node:fs";
11606
11611
  import { join as join2, resolve } from "node:path";
11607
11612
  var BATCH = 10;
@@ -11667,7 +11672,7 @@ async function assetsPush(storeDir) {
11667
11672
  }
11668
11673
  const files = readdirSync(imagesDir).filter((f) => f.endsWith(".png")).map((name) => {
11669
11674
  const bytes = readFileSync2(join2(imagesDir, name));
11670
- return { name, bytes, checksum: createHash("sha256").update(bytes).digest("hex") };
11675
+ return { name, bytes, checksum: sha256(bytes) };
11671
11676
  }).filter((f) => index[f.name]?.checksum !== f.checksum);
11672
11677
  if (!files.length) {
11673
11678
  console.log("Everything already pushed.");
@@ -11848,7 +11853,6 @@ async function workspaceSet(slugOrId) {
11848
11853
  }
11849
11854
 
11850
11855
  // src/images.ts
11851
- import { createHash as createHash2 } from "node:crypto";
11852
11856
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3, rmSync as rmSync2 } from "node:fs";
11853
11857
  import { join as join3, resolve as resolve2 } from "node:path";
11854
11858
  var MAX_JOBS_PER_REQUEST = 40;
@@ -11882,7 +11886,7 @@ async function saveProduced(imagesDir, indexPath, index, file2, asset) {
11882
11886
  index[file2] = {
11883
11887
  assetId: asset.id,
11884
11888
  url: asset.url,
11885
- checksum: createHash2("sha256").update(bytes).digest("hex")
11889
+ checksum: sha256(bytes)
11886
11890
  };
11887
11891
  writeFileSync4(indexPath, JSON.stringify(index, null, 2) + `
11888
11892
  `);
@@ -12057,8 +12061,597 @@ async function imagesGenerate(storeDir) {
12057
12061
  }
12058
12062
 
12059
12063
  // src/shopify.ts
12060
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
12061
- import { basename, join as join4, resolve as resolve3 } from "node:path";
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";
12071
+
12072
+ // src/theme.ts
12073
+ import { spawnSync as spawnSync2 } from "node:child_process";
12074
+ import { randomBytes as randomBytes2 } from "node:crypto";
12075
+ import {
12076
+ cpSync,
12077
+ existsSync as existsSync4,
12078
+ mkdirSync as mkdirSync2,
12079
+ mkdtempSync,
12080
+ readdirSync as readdirSync2,
12081
+ readFileSync as readFileSync4,
12082
+ renameSync,
12083
+ rmSync as rmSync3,
12084
+ statSync,
12085
+ unlinkSync,
12086
+ writeFileSync as writeFileSync5
12087
+ } from "node:fs";
12088
+ import { tmpdir } from "node:os";
12089
+ import { basename, dirname, join as join4, resolve as resolve3 } from "node:path";
12090
+ var TEMPLATES_PKG = "@zalify/theme-templates";
12091
+ var REGISTRY = "https://registry.npmjs.org";
12092
+ var MANIFEST_PATH = ".zalify/theme.json";
12093
+ var PROTECTED_DIRS = ["theme/", ".git/", ".zalify/", "node_modules/", ".next/"];
12094
+ function isProtected(rel) {
12095
+ if (rel === ".env" || rel.startsWith(".env.") && rel !== ".env.example")
12096
+ return true;
12097
+ return PROTECTED_DIRS.some((p) => rel === p.slice(0, -1) || rel.startsWith(p));
12098
+ }
12099
+ async function fetchPackument() {
12100
+ const res = await fetch(`${REGISTRY}/${TEMPLATES_PKG}`, {
12101
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
12102
+ });
12103
+ if (res.status === 404) {
12104
+ throw new Error(`${TEMPLATES_PKG} not found on npm — it may not be published yet.
12105
+ ` + ` For local testing pass --tarball <path-to-.tgz>.`);
12106
+ }
12107
+ if (!res.ok)
12108
+ throw new Error(`npm registry ${res.status} for ${TEMPLATES_PKG}`);
12109
+ return await res.json();
12110
+ }
12111
+ async function downloadTarball(url) {
12112
+ const res = await fetch(url);
12113
+ if (!res.ok)
12114
+ throw new Error(`tarball download ${res.status}: ${url}`);
12115
+ const file2 = join4(mkdtempSync(join4(tmpdir(), "zalify-theme-")), "pkg.tgz");
12116
+ writeFileSync5(file2, Buffer.from(await res.arrayBuffer()));
12117
+ return file2;
12118
+ }
12119
+ function extractTarball(file2) {
12120
+ const dir2 = mkdtempSync(join4(tmpdir(), "zalify-theme-x-"));
12121
+ const result = spawnSync2("tar", ["-xzf", resolve3(file2), "-C", dir2], {
12122
+ encoding: "utf8"
12123
+ });
12124
+ if (result.error || result.status !== 0) {
12125
+ throw new Error(`tar extract failed for ${file2}: ${result.stderr || result.error?.message}`);
12126
+ }
12127
+ const root = join4(dir2, "package");
12128
+ if (!existsSync4(root))
12129
+ throw new Error(`unexpected tarball layout in ${file2} (no package/)`);
12130
+ return root;
12131
+ }
12132
+ async function acquireTemplatePkg(localTarball, version) {
12133
+ if (localTarball) {
12134
+ const root = extractTarball(localTarball);
12135
+ const v = JSON.parse(readFileSync4(join4(root, "package.json"), "utf8")).version;
12136
+ return { root, version: v };
12137
+ }
12138
+ const packument = await fetchPackument();
12139
+ const target = version ?? packument["dist-tags"].latest;
12140
+ const entry = packument.versions[target];
12141
+ if (!entry)
12142
+ throw new Error(`${TEMPLATES_PKG}@${target} does not exist on npm`);
12143
+ return { root: extractTarball(await downloadTarball(entry.dist.tarball)), version: target };
12144
+ }
12145
+ function readTemplateManifest(pkgRoot, template) {
12146
+ const path9 = join4(pkgRoot, "templates", template, MANIFEST_PATH);
12147
+ if (!existsSync4(path9)) {
12148
+ throw new Error(`template "${template}" has no ${MANIFEST_PATH} — this ${TEMPLATES_PKG} version predates the upgrade contract`);
12149
+ }
12150
+ return JSON.parse(readFileSync4(path9, "utf8"));
12151
+ }
12152
+ function resolveVariant(manifest, variant) {
12153
+ const resolved = { ...manifest, files: { ...manifest.files }, variant };
12154
+ if (variant === "editor" && manifest.editorFiles) {
12155
+ Object.assign(resolved.files, manifest.editorFiles);
12156
+ }
12157
+ delete resolved.editorFiles;
12158
+ return resolved;
12159
+ }
12160
+ function templateFilePath(templateDir, rel, variant) {
12161
+ const stashed = rel.replace(/(^|\/)\.gitignore$/, "$1gitignore");
12162
+ if (variant === "editor") {
12163
+ for (const candidate of [rel, stashed]) {
12164
+ const overlay = join4(templateDir, "__editor__", candidate);
12165
+ if (existsSync4(overlay))
12166
+ return overlay;
12167
+ }
12168
+ }
12169
+ for (const candidate of [rel, stashed]) {
12170
+ const path9 = join4(templateDir, candidate);
12171
+ if (existsSync4(path9))
12172
+ return path9;
12173
+ }
12174
+ throw new Error(`file listed in manifest but missing from template: ${rel}`);
12175
+ }
12176
+ function readProjectManifest(dir2) {
12177
+ const path9 = join4(dir2, MANIFEST_PATH);
12178
+ if (!existsSync4(path9)) {
12179
+ throw new Error(`no ${MANIFEST_PATH} in ${dir2} — not a scaffolded Zalify storefront.
12180
+ ` + ` Scaffold one with \`zalify theme create\`.`);
12181
+ }
12182
+ return JSON.parse(readFileSync4(path9, "utf8"));
12183
+ }
12184
+ function writeJson(path9, value) {
12185
+ writeFileSync5(path9, JSON.stringify(value, null, 2) + `
12186
+ `);
12187
+ }
12188
+ async function themeCreate(dir2, opts) {
12189
+ const targetDir = resolve3(dir2);
12190
+ if (existsSync4(targetDir) && readdirSync2(targetDir).length > 0) {
12191
+ throw new Error(`${dir2} already exists and is not empty`);
12192
+ }
12193
+ const { root, version } = await acquireTemplatePkg(opts.tarball, opts.to);
12194
+ const frameworks = JSON.parse(readFileSync4(join4(root, "templates.json"), "utf8"));
12195
+ const template = opts.template ?? "nextjs";
12196
+ const meta = frameworks.frameworks[template];
12197
+ if (!meta) {
12198
+ throw new Error(`unknown template "${template}" — expected one of: ${Object.keys(frameworks.frameworks).join(", ")}`);
12199
+ }
12200
+ const templateDir = join4(root, "templates", template);
12201
+ console.log(`Scaffolding ${meta.label} (${TEMPLATES_PKG}@${version}) into ${dir2}`);
12202
+ mkdirSync2(targetDir, { recursive: true });
12203
+ cpSync(templateDir, targetDir, { recursive: true });
12204
+ const overlayDir = join4(targetDir, "__editor__");
12205
+ const useEditor = Boolean(opts.editor) && existsSync4(overlayDir);
12206
+ if (opts.editor && !useEditor) {
12207
+ console.log(` ! --editor has no effect for ${template} — no editor overlay in this template`);
12208
+ }
12209
+ if (useEditor)
12210
+ cpSync(overlayDir, targetDir, { recursive: true });
12211
+ rmSync3(overlayDir, { recursive: true, force: true });
12212
+ (function restoreGitignores(d) {
12213
+ for (const entry of readdirSync2(d)) {
12214
+ const full = join4(d, entry);
12215
+ if (statSync(full).isDirectory())
12216
+ restoreGitignores(full);
12217
+ else if (entry === "gitignore")
12218
+ renameSync(full, join4(d, ".gitignore"));
12219
+ }
12220
+ })(targetDir);
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";
12224
+ writeJson(pkgPath, pkg);
12225
+ if (meta.env) {
12226
+ const lines = [
12227
+ "# Leave the store variables empty to run against https://mock.shop",
12228
+ "# demo data. Fill them in to connect your Shopify store.",
12229
+ `${meta.env.domain}=${opts.storeDomain ?? ""}`,
12230
+ `${meta.env.token}=${opts.storefrontToken ?? ""}`
12231
+ ];
12232
+ if (meta.env.sessionSecret) {
12233
+ lines.push("# Session cookie signing key — unique to this project.", `${meta.env.sessionSecret}=${randomBytes2(32).toString("hex")}`);
12234
+ }
12235
+ writeFileSync5(join4(targetDir, ".env"), lines.join(`
12236
+ `) + `
12237
+ `);
12238
+ }
12239
+ const manifest = readProjectManifest(targetDir);
12240
+ writeJson(join4(targetDir, MANIFEST_PATH), resolveVariant(manifest, useEditor ? "editor" : "default"));
12241
+ if (opts.git !== false) {
12242
+ const git = (args) => spawnSync2("git", args, { cwd: targetDir, stdio: "ignore" });
12243
+ if (git(["init", "-b", "main"]).status === 0) {
12244
+ git(["add", "-A"]);
12245
+ git(["commit", "-m", "Initial commit from zalify theme create"]);
12246
+ console.log(" ✓ initialized git repository");
12247
+ } else {
12248
+ console.log(" ! git init failed — skipping repository setup");
12249
+ }
12250
+ }
12251
+ if (opts.install !== false) {
12252
+ const pm = spawnSync2("pnpm", ["--version"], { stdio: "ignore" }).status === 0 ? "pnpm" : "npm";
12253
+ console.log(` Installing dependencies with ${pm}…`);
12254
+ const result = spawnSync2(pm, ["install"], { cwd: targetDir, stdio: "inherit" });
12255
+ if (result.status !== 0) {
12256
+ console.log(` ! ${pm} install failed — run it manually inside the project`);
12257
+ }
12258
+ }
12259
+ console.log(`
12260
+ Done. Next steps:`);
12261
+ console.log(` cd ${dir2}`);
12262
+ if (opts.install === false)
12263
+ console.log(" pnpm install");
12264
+ for (const step of meta.nextSteps)
12265
+ console.log(` ${step}`);
12266
+ if (meta.env) {
12267
+ console.log(`
12268
+ Runs on mock.shop demo data out of the box — edit .env to connect your store.`);
12269
+ }
12270
+ console.log("Your theme customizations live in theme/ — see theme/README.md.");
12271
+ }
12272
+ function themeStatus(dir2 = ".") {
12273
+ const projectDir = resolve3(dir2);
12274
+ const manifest = readProjectManifest(projectDir);
12275
+ console.log(`${manifest.template} theme, ${TEMPLATES_PKG}@${manifest.version}` + (manifest.variant === "editor" ? " (editor variant)" : ""));
12276
+ let clean = 0;
12277
+ const edited = [];
12278
+ const missing = [];
12279
+ for (const [rel, expected] of Object.entries(manifest.files)) {
12280
+ const full = join4(projectDir, rel);
12281
+ if (!existsSync4(full))
12282
+ missing.push(rel);
12283
+ else if (sha256(readFileSync4(full)) !== expected)
12284
+ edited.push(rel);
12285
+ else
12286
+ clean++;
12287
+ }
12288
+ for (const rel of edited)
12289
+ console.log(` ! edited ${rel}`);
12290
+ for (const rel of missing)
12291
+ console.log(` ✗ missing ${rel}`);
12292
+ console.log(`${clean} clean, ${edited.length} edited, ${missing.length} missing ` + `of ${Object.keys(manifest.files).length} theme-owned files`);
12293
+ if (edited.length) {
12294
+ console.log("Edited files are 3-way-merged on `zalify theme upgrade` (conflicts marked).");
12295
+ }
12296
+ }
12297
+ function looksBinary(...buffers) {
12298
+ return buffers.some((b) => b.subarray(0, 8000).includes(0));
12299
+ }
12300
+ async function themeUpgrade(dir2 = ".", opts = {}) {
12301
+ const projectDir = resolve3(dir2);
12302
+ const local = readProjectManifest(projectDir);
12303
+ const variant = local.variant ?? "default";
12304
+ const target = await acquireTemplatePkg(opts.tarball, opts.to);
12305
+ if (target.version === local.version && !opts.tarball) {
12306
+ console.log(`Already at ${TEMPLATES_PKG}@${local.version} — nothing to do.`);
12307
+ return;
12308
+ }
12309
+ const base = await acquireTemplatePkg(opts.baseTarball, local.version);
12310
+ if (base.version !== local.version) {
12311
+ console.log(` ! base tarball is ${base.version}, project manifest says ${local.version} — using it anyway`);
12312
+ }
12313
+ const oldManifest = resolveVariant(readTemplateManifest(base.root, local.template), variant);
12314
+ const newManifest = resolveVariant(readTemplateManifest(target.root, local.template), variant);
12315
+ const oldDir = join4(base.root, "templates", local.template);
12316
+ const newDir = join4(target.root, "templates", local.template);
12317
+ console.log(`Upgrading ${local.template} theme ${local.version} → ${target.version}` + (opts.dryRun ? " (dry run)" : ""));
12318
+ const counts = {
12319
+ overwrite: 0,
12320
+ add: 0,
12321
+ merge: 0,
12322
+ conflict: 0,
12323
+ delete: 0,
12324
+ "keep-edited": 0,
12325
+ "keep-warn": 0
12326
+ };
12327
+ const conflicts = [];
12328
+ const allPaths = [...new Set([...Object.keys(oldManifest.files), ...Object.keys(newManifest.files)])].sort();
12329
+ for (const rel of allPaths) {
12330
+ if (isProtected(rel))
12331
+ continue;
12332
+ const oldHash = oldManifest.files[rel];
12333
+ const newHash = newManifest.files[rel];
12334
+ const localPath = join4(projectDir, rel);
12335
+ const localExists = existsSync4(localPath);
12336
+ const localBytes = localExists ? readFileSync4(localPath) : null;
12337
+ const localHash = localBytes ? sha256(localBytes) : null;
12338
+ const write = (bytes) => {
12339
+ if (opts.dryRun)
12340
+ return;
12341
+ mkdirSync2(dirname(localPath), { recursive: true });
12342
+ writeFileSync5(localPath, bytes);
12343
+ };
12344
+ if (oldHash && newHash) {
12345
+ if (oldHash === newHash) {
12346
+ if (!localExists)
12347
+ console.log(` ! missing ${rel} (theme file was deleted locally; unchanged upstream — leaving as is)`);
12348
+ continue;
12349
+ }
12350
+ const newBytes = readFileSync4(templateFilePath(newDir, rel, variant));
12351
+ if (!localExists) {
12352
+ write(newBytes);
12353
+ counts.add++;
12354
+ console.log(` + ${opts.dryRun ? "would restore" : "restored"} ${rel}`);
12355
+ } else if (localHash === oldHash) {
12356
+ write(newBytes);
12357
+ counts.overwrite++;
12358
+ console.log(` ✓ ${opts.dryRun ? "would update" : "updated"} ${rel}`);
12359
+ } else if (localHash === newHash) {} else {
12360
+ const oldBytes = readFileSync4(templateFilePath(oldDir, rel, variant));
12361
+ if (looksBinary(localBytes, oldBytes, newBytes)) {
12362
+ if (!opts.dryRun)
12363
+ writeFileSync5(`${localPath}.new`, newBytes);
12364
+ counts.conflict++;
12365
+ conflicts.push(rel);
12366
+ console.log(` ✗ conflict ${rel} (binary — new version written to ${rel}.new)`);
12367
+ continue;
12368
+ }
12369
+ if (opts.dryRun) {
12370
+ counts.merge++;
12371
+ console.log(` ~ would merge ${rel}`);
12372
+ continue;
12373
+ }
12374
+ const scratch = mkdtempSync(join4(tmpdir(), "zalify-merge-"));
12375
+ const basePath = join4(scratch, "base");
12376
+ const newPath = join4(scratch, "new");
12377
+ writeFileSync5(basePath, oldBytes);
12378
+ writeFileSync5(newPath, newBytes);
12379
+ const result = spawnSync2("git", ["merge-file", "-L", "current", "-L", `base (${base.version})`, "-L", `new (${target.version})`, localPath, basePath, newPath], { encoding: "utf8" });
12380
+ rmSync3(scratch, { recursive: true, force: true });
12381
+ if (result.error || result.status === null || result.status < 0) {
12382
+ writeFileSync5(`${localPath}.new`, newBytes);
12383
+ counts.conflict++;
12384
+ conflicts.push(rel);
12385
+ console.log(` ✗ conflict ${rel} (merge unavailable — new version written to ${rel}.new)`);
12386
+ } else if (result.status > 0) {
12387
+ counts.conflict++;
12388
+ conflicts.push(rel);
12389
+ console.log(` ✗ conflict ${rel} (${result.status} conflict marker(s) left in file)`);
12390
+ } else {
12391
+ counts.merge++;
12392
+ console.log(` ✓ merged ${rel} (kept your edits)`);
12393
+ }
12394
+ }
12395
+ } else if (oldHash && !newHash) {
12396
+ if (!localExists)
12397
+ continue;
12398
+ if (localHash === oldHash) {
12399
+ if (!opts.dryRun)
12400
+ unlinkSync(localPath);
12401
+ counts.delete++;
12402
+ console.log(` - ${opts.dryRun ? "would remove" : "removed"} ${rel} (deleted upstream)`);
12403
+ } else {
12404
+ counts["keep-warn"]++;
12405
+ console.log(` ! kept ${rel} (deleted upstream, but you edited it)`);
12406
+ }
12407
+ } else if (!oldHash && newHash) {
12408
+ if (!localExists) {
12409
+ write(readFileSync4(templateFilePath(newDir, rel, variant)));
12410
+ counts.add++;
12411
+ console.log(` + ${opts.dryRun ? "would add" : "added"} ${rel}`);
12412
+ } else if (localHash !== newHash) {
12413
+ counts["keep-warn"]++;
12414
+ console.log(` ! kept ${rel} (upstream added a file where yours exists)`);
12415
+ }
12416
+ }
12417
+ }
12418
+ const depsChanged = mergePackageJson(projectDir, oldDir, newDir, opts.dryRun ?? false);
12419
+ if (!opts.dryRun) {
12420
+ writeJson(join4(projectDir, MANIFEST_PATH), { ...newManifest, version: target.version });
12421
+ }
12422
+ const summary = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k.replace("-", " ")}`).join(", ");
12423
+ console.log(`
12424
+ ${opts.dryRun ? "Would apply" : "Done"}: ${summary || "no file changes"}.`);
12425
+ if (conflicts.length) {
12426
+ console.log(`Resolve ${conflicts.length} conflict(s), then re-run \`zalify theme status\`:`);
12427
+ for (const rel of conflicts)
12428
+ console.log(` ${rel}`);
12429
+ }
12430
+ if (depsChanged && !opts.dryRun) {
12431
+ if (opts.install) {
12432
+ const pm = spawnSync2("pnpm", ["--version"], { stdio: "ignore" }).status === 0 ? "pnpm" : "npm";
12433
+ console.log(`Dependencies changed — running ${pm} install…`);
12434
+ spawnSync2(pm, ["install"], { cwd: projectDir, stdio: "inherit" });
12435
+ } else {
12436
+ console.log("Dependencies changed — run `pnpm install`.");
12437
+ }
12438
+ }
12439
+ }
12440
+ function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
12441
+ const read = (p) => JSON.parse(readFileSync4(join4(p, "package.json"), "utf8"));
12442
+ const localPkg = read(projectDir);
12443
+ const oldPkg = read(oldDir);
12444
+ const newPkg = read(newDir);
12445
+ const eq = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
12446
+ let changed = false;
12447
+ let depsChanged = false;
12448
+ const mergeSection = (section) => {
12449
+ const localS = localPkg[section] ?? {};
12450
+ const oldS = oldPkg[section] ?? {};
12451
+ const newS = newPkg[section] ?? {};
12452
+ for (const key of new Set([...Object.keys(oldS), ...Object.keys(newS)])) {
12453
+ if (eq(oldS[key], newS[key]))
12454
+ continue;
12455
+ if (!eq(localS[key], oldS[key]) && key in localS) {
12456
+ if (!eq(localS[key], newS[key])) {
12457
+ console.log(` ! kept package.json ${section}.${key} = ${JSON.stringify(localS[key])} (template wants ${JSON.stringify(newS[key] ?? "removed")})`);
12458
+ }
12459
+ continue;
12460
+ }
12461
+ changed = true;
12462
+ if (section === "dependencies" || section === "devDependencies")
12463
+ depsChanged = true;
12464
+ if (key in newS) {
12465
+ localS[key] = newS[key];
12466
+ console.log(` ✓ package.json ${section}.${key} → ${JSON.stringify(newS[key])}`);
12467
+ } else {
12468
+ delete localS[key];
12469
+ console.log(` - package.json ${section}.${key} removed`);
12470
+ }
12471
+ }
12472
+ if (Object.keys(localS).length)
12473
+ localPkg[section] = localS;
12474
+ };
12475
+ for (const section of ["dependencies", "devDependencies", "scripts"])
12476
+ mergeSection(section);
12477
+ for (const key of ["engines", "packageManager", "type"]) {
12478
+ if (eq(oldPkg[key], newPkg[key]))
12479
+ continue;
12480
+ if (!eq(localPkg[key], oldPkg[key]))
12481
+ continue;
12482
+ changed = true;
12483
+ if (key in newPkg)
12484
+ localPkg[key] = newPkg[key];
12485
+ else
12486
+ delete localPkg[key];
12487
+ console.log(` ✓ package.json ${key} updated`);
12488
+ }
12489
+ if (changed && !dryRun) {
12490
+ writeFileSync5(join4(projectDir, "package.json"), JSON.stringify(localPkg, null, 2) + `
12491
+ `);
12492
+ }
12493
+ return depsChanged;
12494
+ }
12495
+
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
12062
12655
  function proxyError(auth, status, json) {
12063
12656
  if (json.code === "SCOPES_MISSING") {
12064
12657
  const granted = new Set(json.grantedScopes ?? []);
@@ -12099,10 +12692,10 @@ async function gql(auth, query, variables = {}) {
12099
12692
  return json.data;
12100
12693
  }
12101
12694
  function readJson(path9, label) {
12102
- if (!existsSync4(path9)) {
12695
+ if (!existsSync6(path9)) {
12103
12696
  throw new Error(`No ${label} at ${path9}`);
12104
12697
  }
12105
- return JSON.parse(readFileSync4(path9, "utf8"));
12698
+ return JSON.parse(readFileSync6(path9, "utf8"));
12106
12699
  }
12107
12700
  function joinErrors(errs) {
12108
12701
  return errs.map((e) => e.message).join("; ");
@@ -12128,8 +12721,8 @@ async function findProductId(auth, handle) {
12128
12721
  }
12129
12722
  async function shopifyImport(storeDir, options = {}) {
12130
12723
  const auth = requireActive();
12131
- const dir2 = resolve3(storeDir);
12132
- const catalog = readJson(join4(dir2, "catalog.json"), "catalog.json");
12724
+ const dir2 = resolve5(storeDir);
12725
+ const catalog = readJson(join6(dir2, "catalog.json"), "catalog.json");
12133
12726
  const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
12134
12727
  const locationId = locations.nodes[0]?.id;
12135
12728
  console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
@@ -12234,15 +12827,16 @@ async function shopifyImport(storeDir, options = {}) {
12234
12827
  console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
12235
12828
  }
12236
12829
  console.log("Done.");
12830
+ await maybeRevalidateStorefront(auth, dir2);
12237
12831
  }
12238
12832
  async function shopifyUploadImages(storeDir) {
12239
12833
  const auth = requireActive();
12240
- const dir2 = resolve3(storeDir);
12241
- const manifest = readJson(join4(dir2, "images", "manifest.json"), "images/manifest.json");
12834
+ const dir2 = resolve5(storeDir);
12835
+ const manifest = readJson(join6(dir2, "images", "manifest.json"), "images/manifest.json");
12242
12836
  console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
12243
12837
  for (const entry of manifest.images) {
12244
- const filePath = join4(dir2, "images", entry.file);
12245
- if (!existsSync4(filePath)) {
12838
+ const filePath = join6(dir2, "images", entry.file);
12839
+ if (!existsSync6(filePath)) {
12246
12840
  console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
12247
12841
  continue;
12248
12842
  }
@@ -12253,13 +12847,13 @@ async function shopifyUploadImages(storeDir) {
12253
12847
  console.error(` ✗ ${entry.handle}: product not found in store`);
12254
12848
  continue;
12255
12849
  }
12256
- const stem = basename(entry.file, ".png");
12850
+ const stem = basename3(entry.file, ".png");
12257
12851
  const already = product.media.nodes.some((m) => m.image?.url?.includes(stem) || entry.alt && m.alt === entry.alt);
12258
12852
  if (already) {
12259
12853
  console.log(` - ${entry.file}: already attached, skipped`);
12260
12854
  continue;
12261
12855
  }
12262
- const bytes = readFileSync4(filePath);
12856
+ const bytes = readFileSync6(filePath);
12263
12857
  const staged = await gql(auth, `mutation($input: [StagedUploadInput!]!) {
12264
12858
  stagedUploadsCreate(input: $input) {
12265
12859
  stagedTargets { url resourceUrl parameters { name value } }
@@ -12269,7 +12863,7 @@ async function shopifyUploadImages(storeDir) {
12269
12863
  input: [
12270
12864
  {
12271
12865
  resource: "IMAGE",
12272
- filename: basename(entry.file),
12866
+ filename: basename3(entry.file),
12273
12867
  mimeType: "image/png",
12274
12868
  httpMethod: "PUT",
12275
12869
  fileSize: String(bytes.length)
@@ -12311,12 +12905,13 @@ async function shopifyUploadImages(storeDir) {
12311
12905
  console.log(` ✓ ${entry.file}`);
12312
12906
  }
12313
12907
  console.log("Done.");
12908
+ await maybeRevalidateStorefront(auth, dir2);
12314
12909
  }
12315
12910
 
12316
12911
  // src/cli.ts
12317
- var __dirname4 = dirname(fileURLToPath3(import.meta.url));
12912
+ var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
12318
12913
  var require2 = createRequire2(import.meta.url);
12319
- var pkg = require2(join5(__dirname4, "..", "package.json"));
12914
+ var pkg = require2(join7(__dirname4, "..", "package.json"));
12320
12915
  try {
12321
12916
  updateNotifier({ pkg }).notify({
12322
12917
  isGlobal: true,
@@ -12371,6 +12966,26 @@ shopify.command("import <store-dir>").description("Import <store-dir>/catalog.js
12371
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) => {
12372
12967
  await shopifyUploadImages(storeDir);
12373
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
+ });
12979
+ var theme = program2.command("theme").description("Scaffold and upgrade standalone Zalify storefronts (Shopify-theme model)");
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) => {
12981
+ await themeCreate(dir2, options);
12982
+ });
12983
+ theme.command("status [dir]").description("List theme-owned files edited since scaffolding (.zalify/theme.json checksums)").action((dir2) => {
12984
+ themeStatus(dir2);
12985
+ });
12986
+ theme.command("upgrade [dir]").description("Upgrade to a newer theme version: untouched files overwritten, edited files 3-way-merged, theme/ + .env never touched").option("--to <version>", "target template version (default: latest)").option("--dry-run", "classify and report without writing anything").option("--install", "run install when dependencies changed").option("--tarball <path>", "target version as a local .tgz (testing)").option("--base-tarball <path>", "base version as a local .tgz (testing)").action(async (dir2, options) => {
12987
+ await themeUpgrade(dir2, options);
12988
+ });
12374
12989
  var assets = program2.command("assets").description("Sync images with the Zalify asset library");
12375
12990
  assets.command("push <store-dir>").description("Upload <store-dir>/images/*.png (sha256 dedup, writes assets.json)").action(async (storeDir) => {
12376
12991
  await assetsPush(storeDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalify/cli",
3
- "version": "0.7.1",
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",