@zalify/cli 0.6.1 → 0.7.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 +259 -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
|
|
11296
|
+
import { dirname, join as join5 } from "node:path";
|
|
11297
11297
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11298
11298
|
|
|
11299
11299
|
// src/auth.ts
|
|
@@ -12056,10 +12056,260 @@ async function imagesGenerate(storeDir) {
|
|
|
12056
12056
|
console.log(`Done: ${counters.produced} generated (already in the asset library), ${counters.failed} failed${counters.failed ? " — re-run to retry" : ""}.`);
|
|
12057
12057
|
}
|
|
12058
12058
|
|
|
12059
|
+
// 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";
|
|
12062
|
+
function proxyError(auth, status, json) {
|
|
12063
|
+
if (status === 404) {
|
|
12064
|
+
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.");
|
|
12065
|
+
}
|
|
12066
|
+
if (status === 402 || json.code === "UPGRADE_REQUIRED") {
|
|
12067
|
+
return new Error(`${json.error ?? "Paid plan required."}
|
|
12068
|
+
Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
|
|
12069
|
+
}
|
|
12070
|
+
if (status === 409) {
|
|
12071
|
+
return new Error(`${json.error ?? "No Shopify store is connected to this workspace."}
|
|
12072
|
+
` + ` Connect one: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`);
|
|
12073
|
+
}
|
|
12074
|
+
return new Error(`shopify/admin ${status}: ${JSON.stringify(json)}`);
|
|
12075
|
+
}
|
|
12076
|
+
async function gql(auth, query, variables = {}) {
|
|
12077
|
+
const res = await fetch(`${auth.appUrl}/api/shopify/admin`, {
|
|
12078
|
+
method: "POST",
|
|
12079
|
+
headers: {
|
|
12080
|
+
"Content-Type": "application/json",
|
|
12081
|
+
Authorization: `Bearer ${auth.key}`
|
|
12082
|
+
},
|
|
12083
|
+
body: JSON.stringify({ workspaceId: auth.workspaceId, query, variables })
|
|
12084
|
+
});
|
|
12085
|
+
const json = await res.json().catch(() => ({}));
|
|
12086
|
+
if (!res.ok)
|
|
12087
|
+
throw proxyError(auth, res.status, json);
|
|
12088
|
+
if (json.errors)
|
|
12089
|
+
throw new Error(JSON.stringify(json.errors));
|
|
12090
|
+
if (!json.data)
|
|
12091
|
+
throw new Error(`shopify/admin: empty response`);
|
|
12092
|
+
return json.data;
|
|
12093
|
+
}
|
|
12094
|
+
function readJson(path9, label) {
|
|
12095
|
+
if (!existsSync4(path9)) {
|
|
12096
|
+
throw new Error(`No ${label} at ${path9}`);
|
|
12097
|
+
}
|
|
12098
|
+
return JSON.parse(readFileSync4(path9, "utf8"));
|
|
12099
|
+
}
|
|
12100
|
+
function joinErrors(errs) {
|
|
12101
|
+
return errs.map((e) => e.message).join("; ");
|
|
12102
|
+
}
|
|
12103
|
+
var PRODUCT_SET = `
|
|
12104
|
+
mutation productSet($input: ProductSetInput!) {
|
|
12105
|
+
productSet(input: $input, synchronous: true) {
|
|
12106
|
+
product { id handle }
|
|
12107
|
+
userErrors { field message }
|
|
12108
|
+
}
|
|
12109
|
+
}`;
|
|
12110
|
+
var COLLECTION_CREATE = `
|
|
12111
|
+
mutation collectionCreate($input: CollectionInput!) {
|
|
12112
|
+
collectionCreate(input: $input) {
|
|
12113
|
+
collection { id handle }
|
|
12114
|
+
userErrors { field message }
|
|
12115
|
+
}
|
|
12116
|
+
}`;
|
|
12117
|
+
async function findProductId(auth, handle) {
|
|
12118
|
+
const data = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${handle}` });
|
|
12119
|
+
const node = data.products.nodes[0];
|
|
12120
|
+
return node && node.handle === handle ? node.id : null;
|
|
12121
|
+
}
|
|
12122
|
+
async function shopifyImport(storeDir, options = {}) {
|
|
12123
|
+
const auth = requireActive();
|
|
12124
|
+
const dir2 = resolve3(storeDir);
|
|
12125
|
+
const catalog = readJson(join4(dir2, "catalog.json"), "catalog.json");
|
|
12126
|
+
const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
|
|
12127
|
+
const locationId = locations.nodes[0]?.id;
|
|
12128
|
+
console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
|
|
12129
|
+
const productIds = [];
|
|
12130
|
+
for (const p of catalog.products) {
|
|
12131
|
+
const options_ = p.options ?? [];
|
|
12132
|
+
const optionValues = options_.map((name, idx) => ({
|
|
12133
|
+
name,
|
|
12134
|
+
position: idx + 1,
|
|
12135
|
+
values: [...new Set(p.variants.map((v) => v.options[idx]))].map((v) => ({ name: v }))
|
|
12136
|
+
}));
|
|
12137
|
+
const input = {
|
|
12138
|
+
handle: p.handle,
|
|
12139
|
+
title: p.title,
|
|
12140
|
+
descriptionHtml: p.descriptionHtml ?? "",
|
|
12141
|
+
vendor: p.vendor ?? catalog.vendor,
|
|
12142
|
+
productType: p.type ?? "",
|
|
12143
|
+
tags: p.tags ?? [],
|
|
12144
|
+
status: "ACTIVE",
|
|
12145
|
+
productOptions: optionValues.length ? optionValues : [{ name: "Title", position: 1, values: [{ name: "Default Title" }] }],
|
|
12146
|
+
variants: p.variants.map((v) => ({
|
|
12147
|
+
optionValues: options_.length ? options_.map((name, idx) => ({ optionName: name, name: v.options[idx] })) : [{ optionName: "Title", name: "Default Title" }],
|
|
12148
|
+
price: v.price,
|
|
12149
|
+
compareAtPrice: v.compareAtPrice ?? null,
|
|
12150
|
+
inventoryItem: {
|
|
12151
|
+
sku: v.sku ?? "",
|
|
12152
|
+
tracked: true,
|
|
12153
|
+
measurement: { weight: { value: v.grams ?? 0, unit: "GRAMS" } }
|
|
12154
|
+
},
|
|
12155
|
+
inventoryQuantities: locationId ? [{ locationId, name: "available", quantity: v.inventory ?? 25 }] : []
|
|
12156
|
+
}))
|
|
12157
|
+
};
|
|
12158
|
+
const existingId = await findProductId(auth, p.handle);
|
|
12159
|
+
if (existingId)
|
|
12160
|
+
input.id = existingId;
|
|
12161
|
+
const data = await gql(auth, PRODUCT_SET, { input });
|
|
12162
|
+
const errs = data.productSet.userErrors;
|
|
12163
|
+
if (errs.length) {
|
|
12164
|
+
console.error(` ✗ ${p.handle}: ${joinErrors(errs)}`);
|
|
12165
|
+
} else if (data.productSet.product) {
|
|
12166
|
+
productIds.push(data.productSet.product.id);
|
|
12167
|
+
console.log(` ✓ ${existingId ? "updated" : "created"} ${p.handle} (${p.variants.length} variants)`);
|
|
12168
|
+
}
|
|
12169
|
+
}
|
|
12170
|
+
if (options.prune) {
|
|
12171
|
+
const wanted = new Set(catalog.products.map((p) => p.handle));
|
|
12172
|
+
const data = await gql(auth, `query($q: String!) { products(first: 100, query: $q) { nodes { id handle } } }`, { q: `vendor:${catalog.vendor}` });
|
|
12173
|
+
for (const node of data.products.nodes) {
|
|
12174
|
+
if (wanted.has(node.handle))
|
|
12175
|
+
continue;
|
|
12176
|
+
const del = await gql(auth, `mutation($input: ProductDeleteInput!) {
|
|
12177
|
+
productDelete(input: $input) { deletedProductId userErrors { field message } }
|
|
12178
|
+
}`, { input: { id: node.id } });
|
|
12179
|
+
const errs = del.productDelete.userErrors;
|
|
12180
|
+
if (errs.length)
|
|
12181
|
+
console.error(` ✗ prune ${node.handle}: ${joinErrors(errs)}`);
|
|
12182
|
+
else
|
|
12183
|
+
console.log(` ✓ pruned ${node.handle}`);
|
|
12184
|
+
}
|
|
12185
|
+
}
|
|
12186
|
+
for (const c of catalog.collections ?? []) {
|
|
12187
|
+
const existing = await gql(auth, `query($q: String!) { collections(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${c.handle}` });
|
|
12188
|
+
if (existing.collections.nodes[0]?.handle === c.handle) {
|
|
12189
|
+
console.log(` - collection ${c.handle} exists, skipped`);
|
|
12190
|
+
continue;
|
|
12191
|
+
}
|
|
12192
|
+
const data = await gql(auth, COLLECTION_CREATE, {
|
|
12193
|
+
input: {
|
|
12194
|
+
handle: c.handle,
|
|
12195
|
+
title: c.title,
|
|
12196
|
+
descriptionHtml: c.descriptionHtml ?? "",
|
|
12197
|
+
ruleSet: {
|
|
12198
|
+
appliedDisjunctively: false,
|
|
12199
|
+
rules: [
|
|
12200
|
+
{
|
|
12201
|
+
column: "TAG",
|
|
12202
|
+
relation: "EQUALS",
|
|
12203
|
+
condition: c.rule?.tag ?? `collection:${c.handle}`
|
|
12204
|
+
}
|
|
12205
|
+
]
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
});
|
|
12209
|
+
const errs = data.collectionCreate.userErrors;
|
|
12210
|
+
if (errs.length)
|
|
12211
|
+
console.error(` ✗ collection ${c.handle}: ${joinErrors(errs)}`);
|
|
12212
|
+
else
|
|
12213
|
+
console.log(` ✓ collection ${c.handle}`);
|
|
12214
|
+
}
|
|
12215
|
+
try {
|
|
12216
|
+
const pubs = await gql(auth, `{ publications(first: 10) { nodes { id name } } }`);
|
|
12217
|
+
const online = pubs.publications.nodes.find((n) => n.name === "Online Store");
|
|
12218
|
+
if (online) {
|
|
12219
|
+
for (const id of productIds) {
|
|
12220
|
+
await gql(auth, `mutation($id: ID!, $input: [PublicationInput!]!) {
|
|
12221
|
+
publishablePublish(id: $id, input: $input) { userErrors { field message } }
|
|
12222
|
+
}`, { id, input: [{ publicationId: online.id }] });
|
|
12223
|
+
}
|
|
12224
|
+
console.log(` ✓ published ${productIds.length} products to Online Store`);
|
|
12225
|
+
}
|
|
12226
|
+
} catch (e) {
|
|
12227
|
+
console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
|
|
12228
|
+
}
|
|
12229
|
+
console.log("Done.");
|
|
12230
|
+
}
|
|
12231
|
+
async function shopifyUploadImages(storeDir) {
|
|
12232
|
+
const auth = requireActive();
|
|
12233
|
+
const dir2 = resolve3(storeDir);
|
|
12234
|
+
const manifest = readJson(join4(dir2, "images", "manifest.json"), "images/manifest.json");
|
|
12235
|
+
console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
|
|
12236
|
+
for (const entry of manifest.images) {
|
|
12237
|
+
const filePath = join4(dir2, "images", entry.file);
|
|
12238
|
+
if (!existsSync4(filePath)) {
|
|
12239
|
+
console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
|
|
12240
|
+
continue;
|
|
12241
|
+
}
|
|
12242
|
+
const found = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle
|
|
12243
|
+
media(first: 50) { nodes { alt ... on MediaImage { image { url } } } } } } }`, { q: `handle:${entry.handle}` });
|
|
12244
|
+
const product = found.products.nodes[0];
|
|
12245
|
+
if (!product || product.handle !== entry.handle) {
|
|
12246
|
+
console.error(` ✗ ${entry.handle}: product not found in store`);
|
|
12247
|
+
continue;
|
|
12248
|
+
}
|
|
12249
|
+
const stem = basename(entry.file, ".png");
|
|
12250
|
+
const already = product.media.nodes.some((m) => m.image?.url?.includes(stem) || entry.alt && m.alt === entry.alt);
|
|
12251
|
+
if (already) {
|
|
12252
|
+
console.log(` - ${entry.file}: already attached, skipped`);
|
|
12253
|
+
continue;
|
|
12254
|
+
}
|
|
12255
|
+
const bytes = readFileSync4(filePath);
|
|
12256
|
+
const staged = await gql(auth, `mutation($input: [StagedUploadInput!]!) {
|
|
12257
|
+
stagedUploadsCreate(input: $input) {
|
|
12258
|
+
stagedTargets { url resourceUrl parameters { name value } }
|
|
12259
|
+
userErrors { field message }
|
|
12260
|
+
}
|
|
12261
|
+
}`, {
|
|
12262
|
+
input: [
|
|
12263
|
+
{
|
|
12264
|
+
resource: "IMAGE",
|
|
12265
|
+
filename: basename(entry.file),
|
|
12266
|
+
mimeType: "image/png",
|
|
12267
|
+
httpMethod: "PUT",
|
|
12268
|
+
fileSize: String(bytes.length)
|
|
12269
|
+
}
|
|
12270
|
+
]
|
|
12271
|
+
});
|
|
12272
|
+
const target = staged.stagedUploadsCreate.stagedTargets?.[0];
|
|
12273
|
+
if (!target) {
|
|
12274
|
+
console.error(` ✗ ${entry.handle}: staged upload failed: ${JSON.stringify(staged.stagedUploadsCreate.userErrors)}`);
|
|
12275
|
+
continue;
|
|
12276
|
+
}
|
|
12277
|
+
const headers = { "Content-Type": "image/png" };
|
|
12278
|
+
for (const p of target.parameters)
|
|
12279
|
+
headers[p.name] = p.value;
|
|
12280
|
+
const up = await fetch(target.url, {
|
|
12281
|
+
method: "PUT",
|
|
12282
|
+
headers,
|
|
12283
|
+
body: new Uint8Array(bytes)
|
|
12284
|
+
});
|
|
12285
|
+
if (!up.ok) {
|
|
12286
|
+
console.error(` ✗ ${entry.handle}: upload PUT failed (${up.status})`);
|
|
12287
|
+
continue;
|
|
12288
|
+
}
|
|
12289
|
+
const media = await gql(auth, `mutation($productId: ID!, $media: [CreateMediaInput!]!) {
|
|
12290
|
+
productCreateMedia(productId: $productId, media: $media) {
|
|
12291
|
+
media { alt }
|
|
12292
|
+
mediaUserErrors { field message }
|
|
12293
|
+
}
|
|
12294
|
+
}`, {
|
|
12295
|
+
productId: product.id,
|
|
12296
|
+
media: [
|
|
12297
|
+
{ originalSource: target.resourceUrl, alt: entry.alt ?? "", mediaContentType: "IMAGE" }
|
|
12298
|
+
]
|
|
12299
|
+
});
|
|
12300
|
+
const errs = media.productCreateMedia.mediaUserErrors;
|
|
12301
|
+
if (errs.length)
|
|
12302
|
+
console.error(` ✗ ${entry.handle}: ${joinErrors(errs)}`);
|
|
12303
|
+
else
|
|
12304
|
+
console.log(` ✓ ${entry.file}`);
|
|
12305
|
+
}
|
|
12306
|
+
console.log("Done.");
|
|
12307
|
+
}
|
|
12308
|
+
|
|
12059
12309
|
// src/cli.ts
|
|
12060
12310
|
var __dirname4 = dirname(fileURLToPath3(import.meta.url));
|
|
12061
12311
|
var require2 = createRequire2(import.meta.url);
|
|
12062
|
-
var pkg = require2(
|
|
12312
|
+
var pkg = require2(join5(__dirname4, "..", "package.json"));
|
|
12063
12313
|
try {
|
|
12064
12314
|
updateNotifier({ pkg }).notify({
|
|
12065
12315
|
isGlobal: true,
|
|
@@ -12107,6 +12357,13 @@ var images = program2.command("images").description("Generate images on Zalify i
|
|
|
12107
12357
|
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) => {
|
|
12108
12358
|
await imagesGenerate(storeDir);
|
|
12109
12359
|
});
|
|
12360
|
+
var shopify = program2.command("shopify").description("Push catalogs and images to the workspace's connected Shopify store");
|
|
12361
|
+
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) => {
|
|
12362
|
+
await shopifyImport(storeDir, options);
|
|
12363
|
+
});
|
|
12364
|
+
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) => {
|
|
12365
|
+
await shopifyUploadImages(storeDir);
|
|
12366
|
+
});
|
|
12110
12367
|
var assets = program2.command("assets").description("Sync images with the Zalify asset library");
|
|
12111
12368
|
assets.command("push <store-dir>").description("Upload <store-dir>/images/*.png (sha256 dedup, writes assets.json)").action(async (storeDir) => {
|
|
12112
12369
|
await assetsPush(storeDir);
|