shopq 0.1.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/LICENSE +21 -0
- package/README.md +157 -0
- package/dist/shopctl.js +2264 -0
- package/dist/shopq.js +2264 -0
- package/package.json +45 -0
package/dist/shopctl.js
ADDED
|
@@ -0,0 +1,2264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/graphql.ts
|
|
4
|
+
var API_VERSION = "2026-01";
|
|
5
|
+
|
|
6
|
+
class GraphQLError extends Error {
|
|
7
|
+
errors;
|
|
8
|
+
constructor(errors) {
|
|
9
|
+
super(errors.map((e) => e.message).join("; "));
|
|
10
|
+
this.errors = errors;
|
|
11
|
+
this.name = "GraphQLError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class HttpError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
body;
|
|
18
|
+
constructor(status, body) {
|
|
19
|
+
super(`HTTP ${status}: ${body}`);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.body = body;
|
|
22
|
+
this.name = "HttpError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ConfigError extends Error {
|
|
27
|
+
missing;
|
|
28
|
+
constructor(missing) {
|
|
29
|
+
super(`Missing required environment variables: ${missing.join(", ")}`);
|
|
30
|
+
this.missing = missing;
|
|
31
|
+
this.name = "ConfigError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function resolveConfig(storeFlag) {
|
|
35
|
+
const missing = [];
|
|
36
|
+
const store = storeFlag || process.env.SHOPIFY_STORE;
|
|
37
|
+
const clientId = process.env.SHOPIFY_CLIENT_ID;
|
|
38
|
+
const clientSecret = process.env.SHOPIFY_CLIENT_SECRET;
|
|
39
|
+
if (!store)
|
|
40
|
+
missing.push("SHOPIFY_STORE");
|
|
41
|
+
if (!clientId)
|
|
42
|
+
missing.push("SHOPIFY_CLIENT_ID");
|
|
43
|
+
if (!clientSecret)
|
|
44
|
+
missing.push("SHOPIFY_CLIENT_SECRET");
|
|
45
|
+
if (missing.length > 0) {
|
|
46
|
+
throw new ConfigError(missing);
|
|
47
|
+
}
|
|
48
|
+
return { store, clientId, clientSecret };
|
|
49
|
+
}
|
|
50
|
+
function sleep(ms) {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
53
|
+
async function exchangeToken(opts) {
|
|
54
|
+
const protocol = opts.protocol ?? "https";
|
|
55
|
+
const url = `${protocol}://${opts.store}/admin/oauth/access_token`;
|
|
56
|
+
const body = new URLSearchParams({
|
|
57
|
+
grant_type: "client_credentials",
|
|
58
|
+
client_id: opts.clientId,
|
|
59
|
+
client_secret: opts.clientSecret
|
|
60
|
+
});
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
64
|
+
body: body.toString()
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
throw new HttpError(response.status, text);
|
|
69
|
+
}
|
|
70
|
+
const json = await response.json();
|
|
71
|
+
return {
|
|
72
|
+
accessToken: json.access_token,
|
|
73
|
+
scope: json.scope,
|
|
74
|
+
expiresIn: json.expires_in
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function createClient(config) {
|
|
78
|
+
const {
|
|
79
|
+
store,
|
|
80
|
+
clientId,
|
|
81
|
+
clientSecret,
|
|
82
|
+
protocol = "https",
|
|
83
|
+
maxRetries = 5,
|
|
84
|
+
timeoutMs = 30000
|
|
85
|
+
} = config;
|
|
86
|
+
const endpoint = `${protocol}://${store}/admin/api/${API_VERSION}/graphql.json`;
|
|
87
|
+
let cachedToken = null;
|
|
88
|
+
let tokenExpiresAt = 0;
|
|
89
|
+
async function getAccessToken() {
|
|
90
|
+
if (cachedToken && Date.now() < tokenExpiresAt) {
|
|
91
|
+
return cachedToken;
|
|
92
|
+
}
|
|
93
|
+
const result = await exchangeToken({
|
|
94
|
+
store,
|
|
95
|
+
clientId,
|
|
96
|
+
clientSecret,
|
|
97
|
+
protocol
|
|
98
|
+
});
|
|
99
|
+
cachedToken = result.accessToken;
|
|
100
|
+
tokenExpiresAt = Date.now() + result.expiresIn * 1000 - 60000;
|
|
101
|
+
return cachedToken;
|
|
102
|
+
}
|
|
103
|
+
async function fetchRaw(query, variables) {
|
|
104
|
+
const accessToken = await getAccessToken();
|
|
105
|
+
const body = JSON.stringify({ query, variables });
|
|
106
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
107
|
+
let response;
|
|
108
|
+
try {
|
|
109
|
+
response = await fetch(endpoint, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
"X-Shopify-Access-Token": accessToken
|
|
114
|
+
},
|
|
115
|
+
body,
|
|
116
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err?.name === "TimeoutError") {
|
|
120
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
if (response.status === 429) {
|
|
125
|
+
if (attempt === maxRetries) {
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
throw new HttpError(429, text);
|
|
128
|
+
}
|
|
129
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
130
|
+
const waitMs = retryAfter ? parseFloat(retryAfter) * 1000 : Math.min(1000 * 2 ** attempt, 30000);
|
|
131
|
+
await sleep(waitMs);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
throw new HttpError(response.status, text);
|
|
137
|
+
}
|
|
138
|
+
return await response.json();
|
|
139
|
+
}
|
|
140
|
+
throw new Error("Unexpected: exhausted retries");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
async query(query, variables) {
|
|
144
|
+
const json = await fetchRaw(query, variables);
|
|
145
|
+
if (json.errors && json.errors.length > 0) {
|
|
146
|
+
throw new GraphQLError(json.errors);
|
|
147
|
+
}
|
|
148
|
+
return json.data;
|
|
149
|
+
},
|
|
150
|
+
async rawQuery(query, variables) {
|
|
151
|
+
return fetchRaw(query, variables);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/helpers.ts
|
|
157
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
158
|
+
|
|
159
|
+
// src/output.ts
|
|
160
|
+
function formatOutput(data, columns, options) {
|
|
161
|
+
if (options.json) {
|
|
162
|
+
const envelope = { data };
|
|
163
|
+
if (options.pageInfo) {
|
|
164
|
+
envelope.pageInfo = options.pageInfo;
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(`${JSON.stringify(envelope, null, 2)}
|
|
167
|
+
`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!Array.isArray(data)) {
|
|
171
|
+
for (const col of columns) {
|
|
172
|
+
if (col.key in data) {
|
|
173
|
+
const label = options.noColor ? col.header : `\x1B[1m${col.header}\x1B[0m`;
|
|
174
|
+
process.stdout.write(`${label}: ${data[col.key]}
|
|
175
|
+
`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const widths = columns.map((col) => {
|
|
181
|
+
const values = data.map((row) => String(row[col.key] ?? ""));
|
|
182
|
+
return Math.max(col.header.length, ...values.map((v) => v.length));
|
|
183
|
+
});
|
|
184
|
+
const headerLine = columns.map((col, i) => {
|
|
185
|
+
const padded = col.header.padEnd(widths[i]);
|
|
186
|
+
return options.noColor ? padded : `\x1B[1m${padded}\x1B[0m`;
|
|
187
|
+
}).join(" ");
|
|
188
|
+
process.stdout.write(`${headerLine}
|
|
189
|
+
`);
|
|
190
|
+
const separator = widths.map((w) => "─".repeat(w)).join(" ");
|
|
191
|
+
process.stdout.write(`${separator}
|
|
192
|
+
`);
|
|
193
|
+
for (const row of data) {
|
|
194
|
+
const line = columns.map((col, i) => {
|
|
195
|
+
return String(row[col.key] ?? "").padEnd(widths[i]);
|
|
196
|
+
}).join(" ");
|
|
197
|
+
process.stdout.write(`${line}
|
|
198
|
+
`);
|
|
199
|
+
}
|
|
200
|
+
if (options.pageInfo?.hasNextPage) {
|
|
201
|
+
process.stdout.write(`
|
|
202
|
+
More results available. Use --cursor ${options.pageInfo.endCursor} to see next page.
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function formatError(message) {
|
|
207
|
+
process.stderr.write(`Error: ${message}
|
|
208
|
+
`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/helpers.ts
|
|
212
|
+
function getClient(flags) {
|
|
213
|
+
const config = resolveConfig(flags.store);
|
|
214
|
+
const protocol = process.env.SHOPIFY_PROTOCOL === "http" ? "http" : "https";
|
|
215
|
+
return createClient({ ...config, protocol });
|
|
216
|
+
}
|
|
217
|
+
function handleCommandError(err) {
|
|
218
|
+
if (err instanceof ConfigError) {
|
|
219
|
+
formatError(err.message);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (err instanceof GraphQLError) {
|
|
224
|
+
formatError(err.message);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (err instanceof Error) {
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
throw new Error(String(err));
|
|
232
|
+
}
|
|
233
|
+
async function readFileText(path) {
|
|
234
|
+
if (!existsSync(path)) {
|
|
235
|
+
formatError(`File not found: ${path}`);
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return readFileSync(path, "utf-8");
|
|
241
|
+
} catch (_err) {
|
|
242
|
+
formatError(`Failed to read file: ${path}`);
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function clampLimit(value, defaultLimit = 50) {
|
|
248
|
+
if (value === undefined)
|
|
249
|
+
return defaultLimit;
|
|
250
|
+
const n = parseInt(value, 10);
|
|
251
|
+
if (Number.isNaN(n) || n < 1) {
|
|
252
|
+
process.stderr.write(`Warning: --limit value "${value}" is invalid, using 1
|
|
253
|
+
`);
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
if (n > 250) {
|
|
257
|
+
process.stderr.write(`Warning: --limit value "${value}" exceeds maximum, using 250
|
|
258
|
+
`);
|
|
259
|
+
return 250;
|
|
260
|
+
}
|
|
261
|
+
return n;
|
|
262
|
+
}
|
|
263
|
+
async function readFileJson(path) {
|
|
264
|
+
const text = await readFileText(path);
|
|
265
|
+
if (text === null)
|
|
266
|
+
return null;
|
|
267
|
+
try {
|
|
268
|
+
return JSON.parse(text);
|
|
269
|
+
} catch (_err) {
|
|
270
|
+
formatError(`Invalid JSON in file: ${path}`);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/registry.ts
|
|
277
|
+
var resources = new Map;
|
|
278
|
+
function register(resourceName, resourceDescription, verbName, command) {
|
|
279
|
+
let resource = resources.get(resourceName);
|
|
280
|
+
if (!resource) {
|
|
281
|
+
resource = {
|
|
282
|
+
name: resourceName,
|
|
283
|
+
description: resourceDescription,
|
|
284
|
+
verbs: new Map
|
|
285
|
+
};
|
|
286
|
+
resources.set(resourceName, resource);
|
|
287
|
+
}
|
|
288
|
+
resource.verbs.set(verbName, command);
|
|
289
|
+
}
|
|
290
|
+
function getResource(name) {
|
|
291
|
+
return resources.get(name);
|
|
292
|
+
}
|
|
293
|
+
function getAllResources() {
|
|
294
|
+
return resources;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/commands/config.ts
|
|
298
|
+
function maskToken(token) {
|
|
299
|
+
if (token.length <= 4)
|
|
300
|
+
return "****";
|
|
301
|
+
return `****${token.slice(-4)}`;
|
|
302
|
+
}
|
|
303
|
+
async function handleConfigShow(parsed) {
|
|
304
|
+
try {
|
|
305
|
+
const config = resolveConfig(parsed.flags.store);
|
|
306
|
+
const data = {
|
|
307
|
+
store: config.store,
|
|
308
|
+
apiVersion: API_VERSION,
|
|
309
|
+
clientId: maskToken(config.clientId),
|
|
310
|
+
clientSecret: maskToken(config.clientSecret)
|
|
311
|
+
};
|
|
312
|
+
const columns = [
|
|
313
|
+
{ key: "store", header: "Store" },
|
|
314
|
+
{ key: "apiVersion", header: "API Version" },
|
|
315
|
+
{ key: "clientId", header: "Client ID" },
|
|
316
|
+
{ key: "clientSecret", header: "Client Secret" }
|
|
317
|
+
];
|
|
318
|
+
formatOutput(data, columns, {
|
|
319
|
+
json: parsed.flags.json,
|
|
320
|
+
noColor: parsed.flags.noColor
|
|
321
|
+
});
|
|
322
|
+
} catch (err) {
|
|
323
|
+
handleCommandError(err);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
register("config", "Configuration management", "show", {
|
|
327
|
+
description: "Show current configuration",
|
|
328
|
+
handler: handleConfigShow
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// src/commands/gql.ts
|
|
332
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
333
|
+
async function handleGql(parsed) {
|
|
334
|
+
let query;
|
|
335
|
+
if (parsed.flags.file) {
|
|
336
|
+
if (!existsSync2(parsed.flags.file)) {
|
|
337
|
+
formatError(`File not found: ${parsed.flags.file}`);
|
|
338
|
+
process.exitCode = 2;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
query = readFileSync2(parsed.flags.file, "utf-8");
|
|
342
|
+
} else if (parsed.args[0] === "-") {
|
|
343
|
+
const chunks = [];
|
|
344
|
+
for await (const chunk of process.stdin)
|
|
345
|
+
chunks.push(chunk);
|
|
346
|
+
query = Buffer.concat(chunks).toString("utf-8");
|
|
347
|
+
} else if (parsed.args[0]) {
|
|
348
|
+
query = parsed.args[0];
|
|
349
|
+
}
|
|
350
|
+
if (!query || query.trim() === "") {
|
|
351
|
+
formatError("No query provided. Pass an inline query, use - for stdin, or use --file <path>.");
|
|
352
|
+
process.exitCode = 2;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let variables;
|
|
356
|
+
if (parsed.flags.vars) {
|
|
357
|
+
try {
|
|
358
|
+
variables = JSON.parse(parsed.flags.vars);
|
|
359
|
+
} catch {
|
|
360
|
+
formatError("Invalid JSON for --vars flag.");
|
|
361
|
+
process.exitCode = 2;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
let client;
|
|
366
|
+
try {
|
|
367
|
+
client = getClient(parsed.flags);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
handleCommandError(err);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const result = await client.rawQuery(query, variables);
|
|
373
|
+
if (result.errors && result.errors.length > 0) {
|
|
374
|
+
process.stderr.write(`${JSON.stringify(result.errors, null, 2)}
|
|
375
|
+
`);
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
380
|
+
`);
|
|
381
|
+
}
|
|
382
|
+
register("gql", "Execute raw GraphQL queries", "_default", {
|
|
383
|
+
description: "Execute a raw GraphQL query or mutation",
|
|
384
|
+
handler: handleGql
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// src/commands/shop.ts
|
|
388
|
+
var SHOP_QUERY = `{
|
|
389
|
+
shop {
|
|
390
|
+
name
|
|
391
|
+
email
|
|
392
|
+
myshopifyDomain
|
|
393
|
+
plan { displayName }
|
|
394
|
+
currencyCode
|
|
395
|
+
taxesIncluded
|
|
396
|
+
billingAddress {
|
|
397
|
+
address1
|
|
398
|
+
city
|
|
399
|
+
province
|
|
400
|
+
country
|
|
401
|
+
zip
|
|
402
|
+
}
|
|
403
|
+
enabledPresentmentCurrencies
|
|
404
|
+
}
|
|
405
|
+
productsCount { count precision }
|
|
406
|
+
}`;
|
|
407
|
+
function formatAddress(addr) {
|
|
408
|
+
if (!addr)
|
|
409
|
+
return "";
|
|
410
|
+
return [addr.address1, addr.city, addr.province, addr.country, addr.zip].filter(Boolean).join(", ");
|
|
411
|
+
}
|
|
412
|
+
async function handleShopGet(parsed) {
|
|
413
|
+
try {
|
|
414
|
+
const client = getClient(parsed.flags);
|
|
415
|
+
const result = await client.query(SHOP_QUERY);
|
|
416
|
+
const shop = result.shop;
|
|
417
|
+
const productsCount = result.productsCount;
|
|
418
|
+
if (parsed.flags.json) {
|
|
419
|
+
const data2 = {
|
|
420
|
+
name: shop.name,
|
|
421
|
+
email: shop.email,
|
|
422
|
+
domain: shop.myshopifyDomain,
|
|
423
|
+
plan: shop.plan.displayName,
|
|
424
|
+
currency: shop.currencyCode,
|
|
425
|
+
taxesIncluded: shop.taxesIncluded,
|
|
426
|
+
billingAddress: shop.billingAddress,
|
|
427
|
+
enabledPresentmentCurrencies: shop.enabledPresentmentCurrencies,
|
|
428
|
+
productsCount
|
|
429
|
+
};
|
|
430
|
+
formatOutput(data2, [], { json: true, noColor: parsed.flags.noColor });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const data = {
|
|
434
|
+
name: shop.name,
|
|
435
|
+
email: shop.email,
|
|
436
|
+
domain: shop.myshopifyDomain,
|
|
437
|
+
plan: shop.plan.displayName,
|
|
438
|
+
currency: shop.currencyCode,
|
|
439
|
+
taxesIncluded: String(shop.taxesIncluded),
|
|
440
|
+
billingAddress: formatAddress(shop.billingAddress),
|
|
441
|
+
enabledPresentmentCurrencies: shop.enabledPresentmentCurrencies.join(", "),
|
|
442
|
+
productsCount: String(productsCount.count)
|
|
443
|
+
};
|
|
444
|
+
const columns = [
|
|
445
|
+
{ key: "name", header: "Name" },
|
|
446
|
+
{ key: "email", header: "Email" },
|
|
447
|
+
{ key: "domain", header: "Domain" },
|
|
448
|
+
{ key: "plan", header: "Plan" },
|
|
449
|
+
{ key: "currency", header: "Currency" },
|
|
450
|
+
{ key: "taxesIncluded", header: "Taxes Included" },
|
|
451
|
+
{ key: "billingAddress", header: "Billing Address" },
|
|
452
|
+
{ key: "enabledPresentmentCurrencies", header: "Presentment Currencies" },
|
|
453
|
+
{ key: "productsCount", header: "Products Count" }
|
|
454
|
+
];
|
|
455
|
+
formatOutput(data, columns, { json: false, noColor: parsed.flags.noColor });
|
|
456
|
+
} catch (err) {
|
|
457
|
+
handleCommandError(err);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
register("shop", "Store information", "get", {
|
|
461
|
+
description: "Show store metadata",
|
|
462
|
+
handler: handleShopGet
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// src/commands/product.ts
|
|
466
|
+
var PRODUCTS_QUERY = `query ProductList($first: Int!, $after: String, $sortKey: ProductSortKeys, $query: String) {
|
|
467
|
+
products(first: $first, after: $after, sortKey: $sortKey, query: $query) {
|
|
468
|
+
edges {
|
|
469
|
+
node {
|
|
470
|
+
id
|
|
471
|
+
title
|
|
472
|
+
status
|
|
473
|
+
productType
|
|
474
|
+
vendor
|
|
475
|
+
variantsCount { count }
|
|
476
|
+
totalInventory
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
pageInfo {
|
|
480
|
+
hasNextPage
|
|
481
|
+
endCursor
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}`;
|
|
485
|
+
function buildQueryFilter(flags) {
|
|
486
|
+
const parts = [];
|
|
487
|
+
if (flags.status)
|
|
488
|
+
parts.push(`status:${flags.status}`);
|
|
489
|
+
if (flags.type)
|
|
490
|
+
parts.push(`product_type:${flags.type}`);
|
|
491
|
+
if (flags.vendor)
|
|
492
|
+
parts.push(`vendor:${flags.vendor}`);
|
|
493
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
494
|
+
}
|
|
495
|
+
async function handleProductList(parsed) {
|
|
496
|
+
try {
|
|
497
|
+
const client = getClient(parsed.flags);
|
|
498
|
+
const limit = clampLimit(parsed.flags.limit);
|
|
499
|
+
const variables = {
|
|
500
|
+
first: limit,
|
|
501
|
+
sortKey: "TITLE",
|
|
502
|
+
query: buildQueryFilter(parsed.flags)
|
|
503
|
+
};
|
|
504
|
+
if (parsed.flags.cursor) {
|
|
505
|
+
variables.after = parsed.flags.cursor;
|
|
506
|
+
}
|
|
507
|
+
const result = await client.query(PRODUCTS_QUERY, variables);
|
|
508
|
+
const products = result.products.edges.map((e) => ({
|
|
509
|
+
id: e.node.id,
|
|
510
|
+
title: e.node.title,
|
|
511
|
+
status: e.node.status,
|
|
512
|
+
productType: e.node.productType,
|
|
513
|
+
vendor: e.node.vendor,
|
|
514
|
+
variantsCount: e.node.variantsCount.count,
|
|
515
|
+
totalInventory: e.node.totalInventory
|
|
516
|
+
}));
|
|
517
|
+
const pageInfo = result.products.pageInfo;
|
|
518
|
+
if (parsed.flags.json) {
|
|
519
|
+
formatOutput(products, [], {
|
|
520
|
+
json: true,
|
|
521
|
+
noColor: parsed.flags.noColor,
|
|
522
|
+
pageInfo
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const columns = [
|
|
527
|
+
{ key: "id", header: "ID" },
|
|
528
|
+
{ key: "title", header: "Title" },
|
|
529
|
+
{ key: "status", header: "Status" },
|
|
530
|
+
{ key: "productType", header: "Type" },
|
|
531
|
+
{ key: "vendor", header: "Vendor" },
|
|
532
|
+
{ key: "variantsCount", header: "Variants" },
|
|
533
|
+
{ key: "totalInventory", header: "Inventory" }
|
|
534
|
+
];
|
|
535
|
+
formatOutput(products, columns, {
|
|
536
|
+
json: false,
|
|
537
|
+
noColor: parsed.flags.noColor,
|
|
538
|
+
pageInfo
|
|
539
|
+
});
|
|
540
|
+
} catch (err) {
|
|
541
|
+
handleCommandError(err);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
var PRODUCT_GET_QUERY = `query ProductGet($id: ID!) {
|
|
545
|
+
product(id: $id) {
|
|
546
|
+
id
|
|
547
|
+
title
|
|
548
|
+
status
|
|
549
|
+
productType
|
|
550
|
+
vendor
|
|
551
|
+
tags
|
|
552
|
+
descriptionHtml
|
|
553
|
+
variants(first: 100) {
|
|
554
|
+
edges {
|
|
555
|
+
node {
|
|
556
|
+
id
|
|
557
|
+
sku
|
|
558
|
+
price
|
|
559
|
+
selectedOptions { name value }
|
|
560
|
+
inventoryQuantity
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
images(first: 20) {
|
|
565
|
+
edges {
|
|
566
|
+
node {
|
|
567
|
+
url
|
|
568
|
+
altText
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}`;
|
|
574
|
+
var PRODUCT_SEARCH_QUERY = `query ProductSearch($query: String!) {
|
|
575
|
+
products(first: 10, query: $query) {
|
|
576
|
+
edges {
|
|
577
|
+
node {
|
|
578
|
+
id
|
|
579
|
+
title
|
|
580
|
+
status
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}`;
|
|
585
|
+
function resolveProductId(input) {
|
|
586
|
+
if (input.startsWith("gid://")) {
|
|
587
|
+
return { type: "gid", id: input };
|
|
588
|
+
}
|
|
589
|
+
if (/^\d+$/.test(input)) {
|
|
590
|
+
return { type: "gid", id: `gid://shopify/Product/${input}` };
|
|
591
|
+
}
|
|
592
|
+
return { type: "title", title: input };
|
|
593
|
+
}
|
|
594
|
+
function stripHtml(html) {
|
|
595
|
+
return html.replace(/<[^>]*>/g, "").trim();
|
|
596
|
+
}
|
|
597
|
+
function truncate(str, max) {
|
|
598
|
+
if (str.length <= max)
|
|
599
|
+
return str;
|
|
600
|
+
return `${str.slice(0, max - 3)}...`;
|
|
601
|
+
}
|
|
602
|
+
async function handleProductGet(parsed) {
|
|
603
|
+
const idOrTitle = parsed.args.join(" ");
|
|
604
|
+
if (!idOrTitle) {
|
|
605
|
+
formatError("Usage: shopctl product get <id-or-title>");
|
|
606
|
+
process.exitCode = 2;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
const client = getClient(parsed.flags);
|
|
611
|
+
const resolved = resolveProductId(idOrTitle);
|
|
612
|
+
if (resolved.type === "gid") {
|
|
613
|
+
const result = await client.query(PRODUCT_GET_QUERY, {
|
|
614
|
+
id: resolved.id
|
|
615
|
+
});
|
|
616
|
+
if (!result.product) {
|
|
617
|
+
formatError(`Product "${idOrTitle}" not found`);
|
|
618
|
+
process.exitCode = 1;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
outputProduct(result.product, parsed);
|
|
622
|
+
} else {
|
|
623
|
+
const searchResult = await client.query(PRODUCT_SEARCH_QUERY, {
|
|
624
|
+
query: `title:${resolved.title}`
|
|
625
|
+
});
|
|
626
|
+
const matches = searchResult.products.edges;
|
|
627
|
+
if (matches.length === 0) {
|
|
628
|
+
formatError(`Product "${idOrTitle}" not found`);
|
|
629
|
+
process.exitCode = 1;
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (matches.length > 1) {
|
|
633
|
+
const columns = [
|
|
634
|
+
{ key: "id", header: "ID" },
|
|
635
|
+
{ key: "title", header: "Title" },
|
|
636
|
+
{ key: "status", header: "Status" }
|
|
637
|
+
];
|
|
638
|
+
const candidates = matches.map((e) => e.node);
|
|
639
|
+
formatOutput(candidates, columns, {
|
|
640
|
+
json: false,
|
|
641
|
+
noColor: parsed.flags.noColor
|
|
642
|
+
});
|
|
643
|
+
process.exitCode = 1;
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const productId = matches[0].node.id;
|
|
647
|
+
const result = await client.query(PRODUCT_GET_QUERY, {
|
|
648
|
+
id: productId
|
|
649
|
+
});
|
|
650
|
+
if (!result.product) {
|
|
651
|
+
formatError(`Product "${idOrTitle}" not found`);
|
|
652
|
+
process.exitCode = 1;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
outputProduct(result.product, parsed);
|
|
656
|
+
}
|
|
657
|
+
} catch (err) {
|
|
658
|
+
handleCommandError(err);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function outputProduct(product, parsed) {
|
|
662
|
+
const variants = product.variants.edges.map((e) => ({
|
|
663
|
+
sku: e.node.sku,
|
|
664
|
+
price: e.node.price,
|
|
665
|
+
options: e.node.selectedOptions.map((o) => `${o.name}: ${o.value}`).join(", "),
|
|
666
|
+
inventoryQuantity: e.node.inventoryQuantity
|
|
667
|
+
}));
|
|
668
|
+
const images = product.images.edges.map((e) => ({
|
|
669
|
+
url: e.node.url,
|
|
670
|
+
alt: e.node.altText ?? ""
|
|
671
|
+
}));
|
|
672
|
+
if (parsed.flags.json) {
|
|
673
|
+
const data = {
|
|
674
|
+
id: product.id,
|
|
675
|
+
title: product.title,
|
|
676
|
+
status: product.status,
|
|
677
|
+
productType: product.productType,
|
|
678
|
+
vendor: product.vendor,
|
|
679
|
+
tags: product.tags,
|
|
680
|
+
description: stripHtml(product.descriptionHtml),
|
|
681
|
+
variants: product.variants.edges.map((e) => ({
|
|
682
|
+
id: e.node.id,
|
|
683
|
+
sku: e.node.sku,
|
|
684
|
+
price: e.node.price,
|
|
685
|
+
options: e.node.selectedOptions,
|
|
686
|
+
inventoryQuantity: e.node.inventoryQuantity
|
|
687
|
+
})),
|
|
688
|
+
images
|
|
689
|
+
};
|
|
690
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const plainDesc = stripHtml(product.descriptionHtml);
|
|
694
|
+
const lines = [];
|
|
695
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
696
|
+
lines.push(`${label("ID")}: ${product.id}`);
|
|
697
|
+
lines.push(`${label("Title")}: ${product.title}`);
|
|
698
|
+
lines.push(`${label("Status")}: ${product.status}`);
|
|
699
|
+
lines.push(`${label("Type")}: ${product.productType}`);
|
|
700
|
+
lines.push(`${label("Vendor")}: ${product.vendor}`);
|
|
701
|
+
lines.push(`${label("Tags")}: ${product.tags.join(", ")}`);
|
|
702
|
+
lines.push(`${label("Description")}: ${truncate(plainDesc, 80)}`);
|
|
703
|
+
lines.push("");
|
|
704
|
+
lines.push(`${label("Variants")}:`);
|
|
705
|
+
for (const v of variants) {
|
|
706
|
+
lines.push(` SKU: ${v.sku} Price: ${v.price} Options: ${v.options} Qty: ${v.inventoryQuantity}`);
|
|
707
|
+
}
|
|
708
|
+
lines.push("");
|
|
709
|
+
lines.push(`${label("Images")}:`);
|
|
710
|
+
for (const img of images) {
|
|
711
|
+
lines.push(` ${img.url}${img.alt ? ` (${img.alt})` : ""}`);
|
|
712
|
+
}
|
|
713
|
+
process.stdout.write(`${lines.join(`
|
|
714
|
+
`)}
|
|
715
|
+
`);
|
|
716
|
+
}
|
|
717
|
+
var PRODUCT_CREATE_MUTATION = `mutation ProductCreate($input: ProductInput!) {
|
|
718
|
+
productCreate(input: $input) {
|
|
719
|
+
product { id }
|
|
720
|
+
userErrors { field message }
|
|
721
|
+
}
|
|
722
|
+
}`;
|
|
723
|
+
var PRODUCT_OPTIONS_CREATE_MUTATION = `mutation ProductOptionsCreate($productId: ID!, $options: [OptionCreateInput!]!) {
|
|
724
|
+
productOptionsCreate(productId: $productId, options: $options) {
|
|
725
|
+
product { id }
|
|
726
|
+
userErrors { field message }
|
|
727
|
+
}
|
|
728
|
+
}`;
|
|
729
|
+
var PRODUCT_VARIANTS_BULK_CREATE_MUTATION = `mutation ProductVariantsBulkCreate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
|
|
730
|
+
productVariantsBulkCreate(productId: $productId, variants: $variants) {
|
|
731
|
+
productVariants { id }
|
|
732
|
+
userErrors { field message }
|
|
733
|
+
}
|
|
734
|
+
}`;
|
|
735
|
+
var PRODUCT_DELETE_MUTATION = `mutation ProductDelete($input: ProductDeleteInput!) {
|
|
736
|
+
productDelete(input: $input) {
|
|
737
|
+
deletedProductId
|
|
738
|
+
userErrors { field message }
|
|
739
|
+
}
|
|
740
|
+
}`;
|
|
741
|
+
async function handleProductCreate(parsed) {
|
|
742
|
+
const { flags } = parsed;
|
|
743
|
+
if (!flags.title) {
|
|
744
|
+
formatError("Missing required flag: --title");
|
|
745
|
+
process.exitCode = 2;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (flags.variants && !flags.options) {
|
|
749
|
+
formatError("--options is required when --variants is provided");
|
|
750
|
+
process.exitCode = 2;
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
let variantsJson = null;
|
|
754
|
+
if (flags.variants) {
|
|
755
|
+
variantsJson = await readFileJson(flags.variants);
|
|
756
|
+
if (variantsJson === null)
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const client = getClient(flags);
|
|
761
|
+
const status = flags.status ? flags.status.toUpperCase() : "DRAFT";
|
|
762
|
+
const input = {
|
|
763
|
+
title: flags.title,
|
|
764
|
+
status
|
|
765
|
+
};
|
|
766
|
+
if (flags.handle)
|
|
767
|
+
input.handle = flags.handle;
|
|
768
|
+
if (flags.type)
|
|
769
|
+
input.productType = flags.type;
|
|
770
|
+
if (flags.vendor)
|
|
771
|
+
input.vendor = flags.vendor;
|
|
772
|
+
if (flags.tags)
|
|
773
|
+
input.tags = flags.tags.split(",").map((t) => t.trim());
|
|
774
|
+
if (flags.description)
|
|
775
|
+
input.descriptionHtml = flags.description;
|
|
776
|
+
const createResult = await client.query(PRODUCT_CREATE_MUTATION, { input });
|
|
777
|
+
if (createResult.productCreate.userErrors.length > 0) {
|
|
778
|
+
formatError(createResult.productCreate.userErrors.map((e) => e.message).join("; "));
|
|
779
|
+
process.exitCode = 1;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const productId = createResult.productCreate.product.id;
|
|
783
|
+
if (!flags.variants) {
|
|
784
|
+
const data2 = { productId };
|
|
785
|
+
if (flags.json) {
|
|
786
|
+
formatOutput(data2, [], { json: true, noColor: flags.noColor });
|
|
787
|
+
} else {
|
|
788
|
+
process.stdout.write(`Created product: ${productId}
|
|
789
|
+
`);
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const optionNames = flags.options.split(",").map((o) => o.trim());
|
|
794
|
+
const optionsInput = optionNames.map((name) => ({
|
|
795
|
+
name,
|
|
796
|
+
values: [{ name: "Default" }]
|
|
797
|
+
}));
|
|
798
|
+
const optionsResult = await client.query(PRODUCT_OPTIONS_CREATE_MUTATION, { productId, options: optionsInput });
|
|
799
|
+
if (optionsResult.productOptionsCreate.userErrors.length > 0) {
|
|
800
|
+
await rollbackProduct(client, productId);
|
|
801
|
+
formatError(`Option creation failed, rollback performed: ${optionsResult.productOptionsCreate.userErrors.map((e) => e.message).join("; ")}`);
|
|
802
|
+
process.exitCode = 1;
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const variantsResult = await client.query(PRODUCT_VARIANTS_BULK_CREATE_MUTATION, {
|
|
806
|
+
productId,
|
|
807
|
+
variants: variantsJson
|
|
808
|
+
});
|
|
809
|
+
if (variantsResult.productVariantsBulkCreate.userErrors.length > 0) {
|
|
810
|
+
await rollbackProduct(client, productId);
|
|
811
|
+
formatError(`Variant creation failed, rollback performed: ${variantsResult.productVariantsBulkCreate.userErrors.map((e) => e.message).join("; ")}`);
|
|
812
|
+
process.exitCode = 1;
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const variantIds = (variantsResult.productVariantsBulkCreate.productVariants ?? []).map((v) => v.id);
|
|
816
|
+
const data = { productId, variantIds };
|
|
817
|
+
if (flags.json) {
|
|
818
|
+
formatOutput(data, [], { json: true, noColor: flags.noColor });
|
|
819
|
+
} else {
|
|
820
|
+
process.stdout.write(`Created product: ${productId}
|
|
821
|
+
`);
|
|
822
|
+
process.stdout.write(`Created variants: ${variantIds.join(", ")}
|
|
823
|
+
`);
|
|
824
|
+
}
|
|
825
|
+
} catch (err) {
|
|
826
|
+
handleCommandError(err);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async function rollbackProduct(client, productId) {
|
|
830
|
+
try {
|
|
831
|
+
await client.query(PRODUCT_DELETE_MUTATION, { input: { id: productId } });
|
|
832
|
+
} catch {}
|
|
833
|
+
}
|
|
834
|
+
var PRODUCT_UPDATE_MUTATION = `mutation ProductUpdate($input: ProductInput!) {
|
|
835
|
+
productUpdate(input: $input) {
|
|
836
|
+
product { id title status productType vendor }
|
|
837
|
+
userErrors { field message }
|
|
838
|
+
}
|
|
839
|
+
}`;
|
|
840
|
+
var UPDATE_FLAGS = [
|
|
841
|
+
"title",
|
|
842
|
+
"description",
|
|
843
|
+
"type",
|
|
844
|
+
"vendor",
|
|
845
|
+
"tags",
|
|
846
|
+
"status"
|
|
847
|
+
];
|
|
848
|
+
async function handleProductUpdate(parsed) {
|
|
849
|
+
const idOrTitle = parsed.args.join(" ");
|
|
850
|
+
if (!idOrTitle) {
|
|
851
|
+
formatError("Usage: shopctl product update <id-or-title> [--title ...] [--status ...] ...");
|
|
852
|
+
process.exitCode = 2;
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const hasUpdateFlag = UPDATE_FLAGS.some((f) => parsed.flags[f] !== undefined);
|
|
856
|
+
if (!hasUpdateFlag) {
|
|
857
|
+
formatError("Provide at least one update flag: --title, --description, --type, --vendor, --tags, --status");
|
|
858
|
+
process.exitCode = 2;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
const client = getClient(parsed.flags);
|
|
863
|
+
const resolved = resolveProductId(idOrTitle);
|
|
864
|
+
let productGid;
|
|
865
|
+
if (resolved.type === "gid") {
|
|
866
|
+
productGid = resolved.id;
|
|
867
|
+
} else {
|
|
868
|
+
const searchResult = await client.query(PRODUCT_SEARCH_QUERY, {
|
|
869
|
+
query: `title:${resolved.title}`
|
|
870
|
+
});
|
|
871
|
+
const matches = searchResult.products.edges;
|
|
872
|
+
if (matches.length === 0) {
|
|
873
|
+
formatError(`Product "${idOrTitle}" not found`);
|
|
874
|
+
process.exitCode = 1;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (matches.length > 1) {
|
|
878
|
+
const columns = [
|
|
879
|
+
{ key: "id", header: "ID" },
|
|
880
|
+
{ key: "title", header: "Title" },
|
|
881
|
+
{ key: "status", header: "Status" }
|
|
882
|
+
];
|
|
883
|
+
formatOutput(matches.map((e) => e.node), columns, { json: false, noColor: parsed.flags.noColor });
|
|
884
|
+
process.exitCode = 1;
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
productGid = matches[0].node.id;
|
|
888
|
+
}
|
|
889
|
+
const input = { id: productGid };
|
|
890
|
+
if (parsed.flags.title !== undefined)
|
|
891
|
+
input.title = parsed.flags.title;
|
|
892
|
+
if (parsed.flags.description !== undefined)
|
|
893
|
+
input.descriptionHtml = parsed.flags.description;
|
|
894
|
+
if (parsed.flags.type !== undefined)
|
|
895
|
+
input.productType = parsed.flags.type;
|
|
896
|
+
if (parsed.flags.vendor !== undefined)
|
|
897
|
+
input.vendor = parsed.flags.vendor;
|
|
898
|
+
if (parsed.flags.tags !== undefined)
|
|
899
|
+
input.tags = parsed.flags.tags.split(",").map((t) => t.trim());
|
|
900
|
+
if (parsed.flags.status !== undefined)
|
|
901
|
+
input.status = parsed.flags.status.toUpperCase();
|
|
902
|
+
const result = await client.query(PRODUCT_UPDATE_MUTATION, { input });
|
|
903
|
+
if (result.productUpdate.userErrors.length > 0) {
|
|
904
|
+
formatError(result.productUpdate.userErrors.map((e) => e.message).join("; "));
|
|
905
|
+
process.exitCode = 1;
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const product = result.productUpdate.product;
|
|
909
|
+
if (parsed.flags.json) {
|
|
910
|
+
formatOutput(product, [], { json: true, noColor: parsed.flags.noColor });
|
|
911
|
+
} else {
|
|
912
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
913
|
+
const lines = [
|
|
914
|
+
`${label("ID")}: ${product.id}`,
|
|
915
|
+
`${label("Title")}: ${product.title}`,
|
|
916
|
+
`${label("Status")}: ${product.status}`,
|
|
917
|
+
`${label("Type")}: ${product.productType}`,
|
|
918
|
+
`${label("Vendor")}: ${product.vendor}`
|
|
919
|
+
];
|
|
920
|
+
process.stdout.write(`${lines.join(`
|
|
921
|
+
`)}
|
|
922
|
+
`);
|
|
923
|
+
}
|
|
924
|
+
} catch (err) {
|
|
925
|
+
handleCommandError(err);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
var PRODUCT_SUMMARY_QUERY = `query ProductSummary($id: ID!) {
|
|
929
|
+
product(id: $id) {
|
|
930
|
+
id
|
|
931
|
+
title
|
|
932
|
+
}
|
|
933
|
+
}`;
|
|
934
|
+
async function handleProductDelete(parsed) {
|
|
935
|
+
const idOrTitle = parsed.args.join(" ");
|
|
936
|
+
if (!idOrTitle) {
|
|
937
|
+
formatError("Usage: shopctl product delete <id-or-title> [--yes]");
|
|
938
|
+
process.exitCode = 2;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
const client = getClient(parsed.flags);
|
|
943
|
+
const resolved = resolveProductId(idOrTitle);
|
|
944
|
+
let productGid;
|
|
945
|
+
let productTitle;
|
|
946
|
+
if (resolved.type === "gid") {
|
|
947
|
+
const result2 = await client.query(PRODUCT_SUMMARY_QUERY, { id: resolved.id });
|
|
948
|
+
if (!result2.product) {
|
|
949
|
+
formatError(`Product "${idOrTitle}" not found`);
|
|
950
|
+
process.exitCode = 1;
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
productGid = result2.product.id;
|
|
954
|
+
productTitle = result2.product.title;
|
|
955
|
+
} else {
|
|
956
|
+
const searchResult = await client.query(PRODUCT_SEARCH_QUERY, {
|
|
957
|
+
query: `title:${resolved.title}`
|
|
958
|
+
});
|
|
959
|
+
const matches = searchResult.products.edges;
|
|
960
|
+
if (matches.length === 0) {
|
|
961
|
+
formatError(`Product "${idOrTitle}" not found`);
|
|
962
|
+
process.exitCode = 1;
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (matches.length > 1) {
|
|
966
|
+
const columns = [
|
|
967
|
+
{ key: "id", header: "ID" },
|
|
968
|
+
{ key: "title", header: "Title" },
|
|
969
|
+
{ key: "status", header: "Status" }
|
|
970
|
+
];
|
|
971
|
+
formatOutput(matches.map((e) => e.node), columns, { json: false, noColor: parsed.flags.noColor });
|
|
972
|
+
process.exitCode = 1;
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
productGid = matches[0].node.id;
|
|
976
|
+
productTitle = matches[0].node.title;
|
|
977
|
+
}
|
|
978
|
+
if (!parsed.flags.yes) {
|
|
979
|
+
const data2 = { id: productGid, title: productTitle };
|
|
980
|
+
if (parsed.flags.json) {
|
|
981
|
+
formatOutput(data2, [], { json: true, noColor: parsed.flags.noColor });
|
|
982
|
+
} else {
|
|
983
|
+
process.stdout.write(`Would delete product: ${productTitle} (${productGid})
|
|
984
|
+
`);
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const result = await client.query(PRODUCT_DELETE_MUTATION, { input: { id: productGid } });
|
|
989
|
+
if (result.productDelete.userErrors.length > 0) {
|
|
990
|
+
formatError(result.productDelete.userErrors.map((e) => e.message).join("; "));
|
|
991
|
+
process.exitCode = 1;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const data = { id: productGid, title: productTitle };
|
|
995
|
+
if (parsed.flags.json) {
|
|
996
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
997
|
+
} else {
|
|
998
|
+
process.stdout.write(`Deleted product: ${productTitle} (${productGid})
|
|
999
|
+
`);
|
|
1000
|
+
}
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
handleCommandError(err);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
register("product", "Product management", "delete", {
|
|
1006
|
+
description: "Delete a product by ID or title",
|
|
1007
|
+
handler: handleProductDelete
|
|
1008
|
+
});
|
|
1009
|
+
register("product", "Product management", "update", {
|
|
1010
|
+
description: "Update a product by ID or title",
|
|
1011
|
+
handler: handleProductUpdate
|
|
1012
|
+
});
|
|
1013
|
+
register("product", "Product management", "create", {
|
|
1014
|
+
description: "Create a product with optional variant support",
|
|
1015
|
+
handler: handleProductCreate
|
|
1016
|
+
});
|
|
1017
|
+
register("product", "Product management", "list", {
|
|
1018
|
+
description: "List products with filtering and pagination",
|
|
1019
|
+
handler: handleProductList
|
|
1020
|
+
});
|
|
1021
|
+
register("product", "Product management", "get", {
|
|
1022
|
+
description: "Get a single product by ID or title",
|
|
1023
|
+
handler: handleProductGet
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// src/commands/menu.ts
|
|
1027
|
+
var MENU_ITEMS_FRAGMENT = `
|
|
1028
|
+
title
|
|
1029
|
+
url
|
|
1030
|
+
type
|
|
1031
|
+
items {
|
|
1032
|
+
title
|
|
1033
|
+
url
|
|
1034
|
+
type
|
|
1035
|
+
items {
|
|
1036
|
+
title
|
|
1037
|
+
url
|
|
1038
|
+
type
|
|
1039
|
+
items {
|
|
1040
|
+
title
|
|
1041
|
+
url
|
|
1042
|
+
type
|
|
1043
|
+
items {
|
|
1044
|
+
title
|
|
1045
|
+
url
|
|
1046
|
+
type
|
|
1047
|
+
items {
|
|
1048
|
+
title
|
|
1049
|
+
url
|
|
1050
|
+
type
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
`;
|
|
1057
|
+
var MENU_BY_ID_QUERY = `query MenuGet($id: ID!) {
|
|
1058
|
+
menu(id: $id) {
|
|
1059
|
+
id
|
|
1060
|
+
title
|
|
1061
|
+
handle
|
|
1062
|
+
items {
|
|
1063
|
+
${MENU_ITEMS_FRAGMENT}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}`;
|
|
1067
|
+
var MENU_BY_HANDLE_QUERY = `query MenuGetByHandle($query: String!) {
|
|
1068
|
+
menus(first: 1, query: $query) {
|
|
1069
|
+
edges {
|
|
1070
|
+
node {
|
|
1071
|
+
id
|
|
1072
|
+
title
|
|
1073
|
+
handle
|
|
1074
|
+
items {
|
|
1075
|
+
${MENU_ITEMS_FRAGMENT}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}`;
|
|
1081
|
+
function resolveMenuId(input) {
|
|
1082
|
+
if (input.startsWith("gid://")) {
|
|
1083
|
+
return { type: "gid", id: input };
|
|
1084
|
+
}
|
|
1085
|
+
if (/^\d+$/.test(input)) {
|
|
1086
|
+
return { type: "gid", id: `gid://shopify/Menu/${input}` };
|
|
1087
|
+
}
|
|
1088
|
+
return { type: "handle", handle: input };
|
|
1089
|
+
}
|
|
1090
|
+
function flattenItems(items, depth, rows) {
|
|
1091
|
+
for (const item of items) {
|
|
1092
|
+
rows.push({
|
|
1093
|
+
indent: " ".repeat(depth),
|
|
1094
|
+
title: item.title,
|
|
1095
|
+
url: item.url,
|
|
1096
|
+
type: item.type
|
|
1097
|
+
});
|
|
1098
|
+
if (item.items && item.items.length > 0) {
|
|
1099
|
+
flattenItems(item.items, depth + 1, rows);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
async function handleMenuGet(parsed) {
|
|
1104
|
+
const idOrHandle = parsed.args.join(" ");
|
|
1105
|
+
if (!idOrHandle) {
|
|
1106
|
+
formatError("Usage: shopctl menu get <id-or-handle>");
|
|
1107
|
+
process.exitCode = 2;
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
const client = getClient(parsed.flags);
|
|
1112
|
+
const resolved = resolveMenuId(idOrHandle);
|
|
1113
|
+
let menu = null;
|
|
1114
|
+
if (resolved.type === "gid") {
|
|
1115
|
+
const result = await client.query(MENU_BY_ID_QUERY, { id: resolved.id });
|
|
1116
|
+
menu = result.menu;
|
|
1117
|
+
} else {
|
|
1118
|
+
const result = await client.query(MENU_BY_HANDLE_QUERY, { query: `handle:${resolved.handle}` });
|
|
1119
|
+
menu = result.menus.edges[0]?.node ?? null;
|
|
1120
|
+
}
|
|
1121
|
+
if (!menu) {
|
|
1122
|
+
formatError(`Menu "${idOrHandle}" not found`);
|
|
1123
|
+
process.exitCode = 1;
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (parsed.flags.json) {
|
|
1127
|
+
const data = {
|
|
1128
|
+
id: menu.id,
|
|
1129
|
+
title: menu.title,
|
|
1130
|
+
handle: menu.handle,
|
|
1131
|
+
itemsCount: menu.items.length,
|
|
1132
|
+
items: menu.items
|
|
1133
|
+
};
|
|
1134
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
1138
|
+
const lines = [];
|
|
1139
|
+
lines.push(`${label("ID")}: ${menu.id}`);
|
|
1140
|
+
lines.push(`${label("Title")}: ${menu.title}`);
|
|
1141
|
+
lines.push(`${label("Handle")}: ${menu.handle}`);
|
|
1142
|
+
lines.push(`${label("Items Count")}: ${menu.items.length}`);
|
|
1143
|
+
lines.push("");
|
|
1144
|
+
lines.push(`${label("Items")}:`);
|
|
1145
|
+
const rows = [];
|
|
1146
|
+
flattenItems(menu.items, 0, rows);
|
|
1147
|
+
for (const row of rows) {
|
|
1148
|
+
lines.push(`${row.indent}${row.title} (${row.type}) ${row.url}`);
|
|
1149
|
+
}
|
|
1150
|
+
process.stdout.write(`${lines.join(`
|
|
1151
|
+
`)}
|
|
1152
|
+
`);
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
handleCommandError(err);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
var MENUS_LIST_QUERY = `query MenuList {
|
|
1158
|
+
menus(first: 250) {
|
|
1159
|
+
edges {
|
|
1160
|
+
node {
|
|
1161
|
+
id
|
|
1162
|
+
title
|
|
1163
|
+
handle
|
|
1164
|
+
items {
|
|
1165
|
+
${MENU_ITEMS_FRAGMENT}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}`;
|
|
1171
|
+
async function handleMenuList(parsed) {
|
|
1172
|
+
try {
|
|
1173
|
+
const client = getClient(parsed.flags);
|
|
1174
|
+
const result = await client.query(MENUS_LIST_QUERY, {});
|
|
1175
|
+
const menus = result.menus.edges.map((e) => e.node);
|
|
1176
|
+
if (parsed.flags.json) {
|
|
1177
|
+
const data = menus.map((m) => ({
|
|
1178
|
+
id: m.id,
|
|
1179
|
+
title: m.title,
|
|
1180
|
+
handle: m.handle,
|
|
1181
|
+
itemCount: m.items.length,
|
|
1182
|
+
items: m.items
|
|
1183
|
+
}));
|
|
1184
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
1188
|
+
const lines = [];
|
|
1189
|
+
for (let i = 0;i < menus.length; i++) {
|
|
1190
|
+
const menu = menus[i];
|
|
1191
|
+
if (i > 0)
|
|
1192
|
+
lines.push("");
|
|
1193
|
+
lines.push(`${label("ID")}: ${menu.id}`);
|
|
1194
|
+
lines.push(`${label("Title")}: ${menu.title}`);
|
|
1195
|
+
lines.push(`${label("Handle")}: ${menu.handle}`);
|
|
1196
|
+
lines.push(`${label("Items Count")}: ${menu.items.length}`);
|
|
1197
|
+
if (menu.items.length > 0) {
|
|
1198
|
+
lines.push(`${label("Items")}:`);
|
|
1199
|
+
const rows = [];
|
|
1200
|
+
flattenItems(menu.items, 0, rows);
|
|
1201
|
+
for (const row of rows) {
|
|
1202
|
+
lines.push(` ${row.indent}${row.title} (${row.type}) ${row.url}`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
process.stdout.write(`${lines.join(`
|
|
1207
|
+
`)}
|
|
1208
|
+
`);
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
handleCommandError(err);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
register("menu", "Navigation menu management", "get", {
|
|
1214
|
+
description: "Get a single menu by ID or handle",
|
|
1215
|
+
handler: handleMenuGet
|
|
1216
|
+
});
|
|
1217
|
+
register("menu", "Navigation menu management", "list", {
|
|
1218
|
+
description: "List all navigation menus",
|
|
1219
|
+
handler: handleMenuList
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// src/commands/file.ts
|
|
1223
|
+
var VALID_TYPES = ["IMAGE", "VIDEO", "GENERIC_FILE"];
|
|
1224
|
+
var FILES_QUERY = `query FileList($first: Int!, $after: String, $query: String) {
|
|
1225
|
+
files(first: $first, after: $after, query: $query) {
|
|
1226
|
+
edges {
|
|
1227
|
+
node {
|
|
1228
|
+
... on MediaImage {
|
|
1229
|
+
id
|
|
1230
|
+
alt
|
|
1231
|
+
mediaContentType
|
|
1232
|
+
fileStatus
|
|
1233
|
+
image { url }
|
|
1234
|
+
createdAt
|
|
1235
|
+
originalSource { fileSize }
|
|
1236
|
+
}
|
|
1237
|
+
... on GenericFile {
|
|
1238
|
+
id
|
|
1239
|
+
alt
|
|
1240
|
+
mimeType
|
|
1241
|
+
fileStatus
|
|
1242
|
+
url
|
|
1243
|
+
createdAt
|
|
1244
|
+
originalFileSize
|
|
1245
|
+
}
|
|
1246
|
+
... on Video {
|
|
1247
|
+
id
|
|
1248
|
+
alt
|
|
1249
|
+
mediaContentType
|
|
1250
|
+
fileStatus
|
|
1251
|
+
sources { url }
|
|
1252
|
+
createdAt
|
|
1253
|
+
originalSource { fileSize }
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
pageInfo {
|
|
1258
|
+
hasNextPage
|
|
1259
|
+
endCursor
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}`;
|
|
1263
|
+
function extractUrl(node) {
|
|
1264
|
+
if (node.image?.url)
|
|
1265
|
+
return node.image.url;
|
|
1266
|
+
if (node.url)
|
|
1267
|
+
return node.url;
|
|
1268
|
+
if (node.sources?.[0]?.url)
|
|
1269
|
+
return node.sources[0].url;
|
|
1270
|
+
return "";
|
|
1271
|
+
}
|
|
1272
|
+
function extractFilename(url) {
|
|
1273
|
+
if (!url)
|
|
1274
|
+
return "";
|
|
1275
|
+
try {
|
|
1276
|
+
const pathname = new URL(url).pathname;
|
|
1277
|
+
return pathname.split("/").pop() ?? "";
|
|
1278
|
+
} catch {
|
|
1279
|
+
return url.split("/").pop() ?? "";
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
async function handleFileList(parsed) {
|
|
1283
|
+
const { flags } = parsed;
|
|
1284
|
+
if (flags.type) {
|
|
1285
|
+
const typeUpper = flags.type.toUpperCase();
|
|
1286
|
+
if (!VALID_TYPES.includes(typeUpper)) {
|
|
1287
|
+
formatError(`Invalid --type "${flags.type}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
1288
|
+
process.exitCode = 2;
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
const client = getClient(flags);
|
|
1294
|
+
const limit = clampLimit(flags.limit);
|
|
1295
|
+
const queryParts = [];
|
|
1296
|
+
if (flags.type)
|
|
1297
|
+
queryParts.push(`media_type:${flags.type.toUpperCase()}`);
|
|
1298
|
+
const variables = {
|
|
1299
|
+
first: limit,
|
|
1300
|
+
query: queryParts.length > 0 ? queryParts.join(" ") : undefined
|
|
1301
|
+
};
|
|
1302
|
+
if (flags.cursor) {
|
|
1303
|
+
variables.after = flags.cursor;
|
|
1304
|
+
}
|
|
1305
|
+
const result = await client.query(FILES_QUERY, variables);
|
|
1306
|
+
const files = result.files.edges.map((e) => {
|
|
1307
|
+
const url = extractUrl(e.node);
|
|
1308
|
+
const mediaType = e.node.mediaContentType ?? e.node.mimeType ?? "";
|
|
1309
|
+
const fileSize = e.node.originalSource?.fileSize ?? (e.node.originalFileSize != null ? String(e.node.originalFileSize) : "");
|
|
1310
|
+
return {
|
|
1311
|
+
id: e.node.id,
|
|
1312
|
+
filename: extractFilename(url),
|
|
1313
|
+
url,
|
|
1314
|
+
alt: e.node.alt ?? "",
|
|
1315
|
+
mediaType,
|
|
1316
|
+
fileSize,
|
|
1317
|
+
createdAt: e.node.createdAt
|
|
1318
|
+
};
|
|
1319
|
+
});
|
|
1320
|
+
const pageInfo = result.files.pageInfo;
|
|
1321
|
+
if (flags.json) {
|
|
1322
|
+
formatOutput(files, [], { json: true, noColor: flags.noColor, pageInfo });
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const columns = [
|
|
1326
|
+
{ key: "id", header: "ID" },
|
|
1327
|
+
{ key: "filename", header: "Filename" },
|
|
1328
|
+
{ key: "url", header: "URL" },
|
|
1329
|
+
{ key: "alt", header: "Alt" },
|
|
1330
|
+
{ key: "mediaType", header: "Type" },
|
|
1331
|
+
{ key: "fileSize", header: "Size" },
|
|
1332
|
+
{ key: "createdAt", header: "Created" }
|
|
1333
|
+
];
|
|
1334
|
+
formatOutput(files, columns, {
|
|
1335
|
+
json: false,
|
|
1336
|
+
noColor: flags.noColor,
|
|
1337
|
+
pageInfo
|
|
1338
|
+
});
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
handleCommandError(err);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
register("file", "File management", "list", {
|
|
1344
|
+
description: "List store files with filtering and pagination",
|
|
1345
|
+
handler: handleFileList
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// src/commands/page.ts
|
|
1349
|
+
var PAGE_CREATE_MUTATION = `mutation PageCreate($page: PageCreateInput!) {
|
|
1350
|
+
pageCreate(page: $page) {
|
|
1351
|
+
page { id handle }
|
|
1352
|
+
userErrors { field message }
|
|
1353
|
+
}
|
|
1354
|
+
}`;
|
|
1355
|
+
async function handlePageCreate(parsed) {
|
|
1356
|
+
const { flags } = parsed;
|
|
1357
|
+
if (!flags.title) {
|
|
1358
|
+
formatError("Missing required flag: --title");
|
|
1359
|
+
process.exitCode = 2;
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (flags.body && flags["body-file"]) {
|
|
1363
|
+
formatError("--body and --body-file are mutually exclusive; provide only one");
|
|
1364
|
+
process.exitCode = 2;
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
try {
|
|
1368
|
+
const client = getClient(flags);
|
|
1369
|
+
const page = {
|
|
1370
|
+
title: flags.title,
|
|
1371
|
+
isPublished: flags.published === "true"
|
|
1372
|
+
};
|
|
1373
|
+
if (flags.handle)
|
|
1374
|
+
page.handle = flags.handle;
|
|
1375
|
+
if (flags.body) {
|
|
1376
|
+
page.body = flags.body;
|
|
1377
|
+
} else if (flags["body-file"]) {
|
|
1378
|
+
const bodyContent = await readFileText(flags["body-file"]);
|
|
1379
|
+
if (bodyContent === null)
|
|
1380
|
+
return;
|
|
1381
|
+
page.body = bodyContent;
|
|
1382
|
+
}
|
|
1383
|
+
const metafields = [];
|
|
1384
|
+
if (flags["seo-title"]) {
|
|
1385
|
+
metafields.push({
|
|
1386
|
+
namespace: "global",
|
|
1387
|
+
key: "title_tag",
|
|
1388
|
+
type: "single_line_text_field",
|
|
1389
|
+
value: flags["seo-title"]
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (flags["seo-desc"]) {
|
|
1393
|
+
metafields.push({
|
|
1394
|
+
namespace: "global",
|
|
1395
|
+
key: "description_tag",
|
|
1396
|
+
type: "single_line_text_field",
|
|
1397
|
+
value: flags["seo-desc"]
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
if (metafields.length > 0) {
|
|
1401
|
+
page.metafields = metafields;
|
|
1402
|
+
}
|
|
1403
|
+
const result = await client.query(PAGE_CREATE_MUTATION, { page });
|
|
1404
|
+
if (result.pageCreate.userErrors.length > 0) {
|
|
1405
|
+
formatError(result.pageCreate.userErrors.map((e) => e.message).join("; "));
|
|
1406
|
+
process.exitCode = 1;
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const created = result.pageCreate.page;
|
|
1410
|
+
const data = { id: created.id, handle: created.handle };
|
|
1411
|
+
if (flags.json) {
|
|
1412
|
+
formatOutput(data, [], { json: true, noColor: flags.noColor });
|
|
1413
|
+
} else {
|
|
1414
|
+
process.stdout.write(`Created page: ${created.handle} (${created.id})
|
|
1415
|
+
`);
|
|
1416
|
+
}
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
handleCommandError(err);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
var PAGES_QUERY = `query PageList($first: Int!, $after: String) {
|
|
1422
|
+
pages(first: $first, after: $after) {
|
|
1423
|
+
edges {
|
|
1424
|
+
node {
|
|
1425
|
+
id
|
|
1426
|
+
title
|
|
1427
|
+
handle
|
|
1428
|
+
isPublished
|
|
1429
|
+
bodySummary
|
|
1430
|
+
createdAt
|
|
1431
|
+
metafields(first: 10, namespace: "global") {
|
|
1432
|
+
edges {
|
|
1433
|
+
node {
|
|
1434
|
+
namespace
|
|
1435
|
+
key
|
|
1436
|
+
value
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
pageInfo {
|
|
1443
|
+
hasNextPage
|
|
1444
|
+
endCursor
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}`;
|
|
1448
|
+
function extractSeo(metafields) {
|
|
1449
|
+
let title = null;
|
|
1450
|
+
let description = null;
|
|
1451
|
+
for (const { node } of metafields.edges) {
|
|
1452
|
+
if (node.namespace === "global" && node.key === "title_tag")
|
|
1453
|
+
title = node.value;
|
|
1454
|
+
if (node.namespace === "global" && node.key === "description_tag")
|
|
1455
|
+
description = node.value;
|
|
1456
|
+
}
|
|
1457
|
+
return { title, description };
|
|
1458
|
+
}
|
|
1459
|
+
async function handlePageList(parsed) {
|
|
1460
|
+
try {
|
|
1461
|
+
const client = getClient(parsed.flags);
|
|
1462
|
+
const limit = clampLimit(parsed.flags.limit);
|
|
1463
|
+
const variables = { first: limit };
|
|
1464
|
+
if (parsed.flags.cursor)
|
|
1465
|
+
variables.after = parsed.flags.cursor;
|
|
1466
|
+
const result = await client.query(PAGES_QUERY, variables);
|
|
1467
|
+
const pages = result.pages.edges.map((e) => {
|
|
1468
|
+
const seo = extractSeo(e.node.metafields);
|
|
1469
|
+
return {
|
|
1470
|
+
id: e.node.id,
|
|
1471
|
+
title: e.node.title,
|
|
1472
|
+
handle: e.node.handle,
|
|
1473
|
+
published: e.node.isPublished,
|
|
1474
|
+
bodySummary: e.node.bodySummary,
|
|
1475
|
+
seo,
|
|
1476
|
+
createdAt: e.node.createdAt
|
|
1477
|
+
};
|
|
1478
|
+
});
|
|
1479
|
+
const pageInfo = result.pages.pageInfo;
|
|
1480
|
+
if (parsed.flags.json) {
|
|
1481
|
+
formatOutput(pages, [], {
|
|
1482
|
+
json: true,
|
|
1483
|
+
noColor: parsed.flags.noColor,
|
|
1484
|
+
pageInfo
|
|
1485
|
+
});
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const columns = [
|
|
1489
|
+
{ key: "id", header: "ID" },
|
|
1490
|
+
{ key: "title", header: "Title" },
|
|
1491
|
+
{ key: "handle", header: "Handle" },
|
|
1492
|
+
{ key: "published", header: "Published" },
|
|
1493
|
+
{ key: "seoTitle", header: "SEO Title" },
|
|
1494
|
+
{ key: "createdAt", header: "Created" }
|
|
1495
|
+
];
|
|
1496
|
+
const tableData = pages.map((p) => ({
|
|
1497
|
+
...p,
|
|
1498
|
+
seoTitle: p.seo.title ?? ""
|
|
1499
|
+
}));
|
|
1500
|
+
formatOutput(tableData, columns, {
|
|
1501
|
+
json: false,
|
|
1502
|
+
noColor: parsed.flags.noColor,
|
|
1503
|
+
pageInfo
|
|
1504
|
+
});
|
|
1505
|
+
} catch (err) {
|
|
1506
|
+
handleCommandError(err);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
var PAGE_FIELDS = `
|
|
1510
|
+
id
|
|
1511
|
+
title
|
|
1512
|
+
handle
|
|
1513
|
+
isPublished
|
|
1514
|
+
body
|
|
1515
|
+
createdAt
|
|
1516
|
+
updatedAt
|
|
1517
|
+
metafields(first: 10, namespace: "global") {
|
|
1518
|
+
edges {
|
|
1519
|
+
node {
|
|
1520
|
+
namespace
|
|
1521
|
+
key
|
|
1522
|
+
value
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}`;
|
|
1526
|
+
var PAGE_GET_BY_ID_QUERY = `query PageGetById($id: ID!) {
|
|
1527
|
+
page(id: $id) {
|
|
1528
|
+
${PAGE_FIELDS}
|
|
1529
|
+
}
|
|
1530
|
+
}`;
|
|
1531
|
+
var PAGE_GET_BY_HANDLE_QUERY = `query PageGetByHandle($query: String!) {
|
|
1532
|
+
pages(first: 1, query: $query) {
|
|
1533
|
+
edges {
|
|
1534
|
+
node {
|
|
1535
|
+
${PAGE_FIELDS}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}`;
|
|
1540
|
+
function resolvePageId(input) {
|
|
1541
|
+
if (input.startsWith("gid://")) {
|
|
1542
|
+
return { type: "gid", id: input };
|
|
1543
|
+
}
|
|
1544
|
+
if (/^\d+$/.test(input)) {
|
|
1545
|
+
return { type: "gid", id: `gid://shopify/Page/${input}` };
|
|
1546
|
+
}
|
|
1547
|
+
return { type: "handle", handle: input };
|
|
1548
|
+
}
|
|
1549
|
+
async function handlePageGet(parsed) {
|
|
1550
|
+
const idOrHandle = parsed.args.join(" ");
|
|
1551
|
+
if (!idOrHandle) {
|
|
1552
|
+
formatError("Usage: shopctl page get <id-or-handle>");
|
|
1553
|
+
process.exitCode = 2;
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
const client = getClient(parsed.flags);
|
|
1558
|
+
const resolved = resolvePageId(idOrHandle);
|
|
1559
|
+
let page = null;
|
|
1560
|
+
if (resolved.type === "gid") {
|
|
1561
|
+
const result = await client.query(PAGE_GET_BY_ID_QUERY, { id: resolved.id });
|
|
1562
|
+
page = result.page;
|
|
1563
|
+
} else {
|
|
1564
|
+
const result = await client.query(PAGE_GET_BY_HANDLE_QUERY, { query: `handle:${resolved.handle}` });
|
|
1565
|
+
page = result.pages.edges[0]?.node ?? null;
|
|
1566
|
+
}
|
|
1567
|
+
if (!page) {
|
|
1568
|
+
formatError(`Page "${idOrHandle}" not found`);
|
|
1569
|
+
process.exitCode = 1;
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const seo = extractSeo(page.metafields);
|
|
1573
|
+
if (parsed.flags.json) {
|
|
1574
|
+
const data = {
|
|
1575
|
+
id: page.id,
|
|
1576
|
+
title: page.title,
|
|
1577
|
+
handle: page.handle,
|
|
1578
|
+
published: page.isPublished,
|
|
1579
|
+
body: page.body,
|
|
1580
|
+
seo,
|
|
1581
|
+
createdAt: page.createdAt,
|
|
1582
|
+
updatedAt: page.updatedAt
|
|
1583
|
+
};
|
|
1584
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
1588
|
+
const lines = [];
|
|
1589
|
+
lines.push(`${label("ID")}: ${page.id}`);
|
|
1590
|
+
lines.push(`${label("Title")}: ${page.title}`);
|
|
1591
|
+
lines.push(`${label("Handle")}: ${page.handle}`);
|
|
1592
|
+
lines.push(`${label("Published")}: ${page.isPublished}`);
|
|
1593
|
+
lines.push(`${label("SEO Title")}: ${seo.title ?? ""}`);
|
|
1594
|
+
lines.push(`${label("SEO Description")}: ${seo.description ?? ""}`);
|
|
1595
|
+
lines.push(`${label("Created")}: ${page.createdAt}`);
|
|
1596
|
+
lines.push(`${label("Updated")}: ${page.updatedAt}`);
|
|
1597
|
+
lines.push("");
|
|
1598
|
+
lines.push(`${label("Body")}:`);
|
|
1599
|
+
lines.push(page.body);
|
|
1600
|
+
process.stdout.write(`${lines.join(`
|
|
1601
|
+
`)}
|
|
1602
|
+
`);
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
handleCommandError(err);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
register("page", "Page management", "get", {
|
|
1608
|
+
description: "Get a single page by handle",
|
|
1609
|
+
handler: handlePageGet
|
|
1610
|
+
});
|
|
1611
|
+
register("page", "Page management", "list", {
|
|
1612
|
+
description: "List static store pages",
|
|
1613
|
+
handler: handlePageList
|
|
1614
|
+
});
|
|
1615
|
+
register("page", "Page management", "create", {
|
|
1616
|
+
description: "Create a static store page",
|
|
1617
|
+
handler: handlePageCreate
|
|
1618
|
+
});
|
|
1619
|
+
var PAGE_LOOKUP_QUERY = `query PageLookup($query: String!) {
|
|
1620
|
+
pages(first: 1, query: $query) {
|
|
1621
|
+
edges {
|
|
1622
|
+
node { id }
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}`;
|
|
1626
|
+
var PAGE_UPDATE_MUTATION = `mutation PageUpdate($id: ID!, $page: PageUpdateInput!) {
|
|
1627
|
+
pageUpdate(id: $id, page: $page) {
|
|
1628
|
+
page { id }
|
|
1629
|
+
userErrors { field message }
|
|
1630
|
+
}
|
|
1631
|
+
}`;
|
|
1632
|
+
async function handlePageUpdate(parsed) {
|
|
1633
|
+
const { flags } = parsed;
|
|
1634
|
+
const handle = parsed.args.join(" ");
|
|
1635
|
+
if (!handle) {
|
|
1636
|
+
formatError("Usage: shopctl page update <handle>");
|
|
1637
|
+
process.exitCode = 2;
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (flags.body && flags["body-file"]) {
|
|
1641
|
+
formatError("--body and --body-file are mutually exclusive; provide only one");
|
|
1642
|
+
process.exitCode = 2;
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const page = {};
|
|
1646
|
+
const updatedFields = [];
|
|
1647
|
+
if (flags.title) {
|
|
1648
|
+
page.title = flags.title;
|
|
1649
|
+
updatedFields.push("title");
|
|
1650
|
+
}
|
|
1651
|
+
if (flags.body) {
|
|
1652
|
+
page.body = flags.body;
|
|
1653
|
+
updatedFields.push("body");
|
|
1654
|
+
} else if (flags["body-file"]) {
|
|
1655
|
+
const bodyContent = await readFileText(flags["body-file"]);
|
|
1656
|
+
if (bodyContent === null)
|
|
1657
|
+
return;
|
|
1658
|
+
page.body = bodyContent;
|
|
1659
|
+
updatedFields.push("body");
|
|
1660
|
+
}
|
|
1661
|
+
const metafields = [];
|
|
1662
|
+
if (flags["seo-title"]) {
|
|
1663
|
+
metafields.push({
|
|
1664
|
+
namespace: "global",
|
|
1665
|
+
key: "title_tag",
|
|
1666
|
+
type: "single_line_text_field",
|
|
1667
|
+
value: flags["seo-title"]
|
|
1668
|
+
});
|
|
1669
|
+
updatedFields.push("seo_title");
|
|
1670
|
+
}
|
|
1671
|
+
if (flags["seo-desc"]) {
|
|
1672
|
+
metafields.push({
|
|
1673
|
+
namespace: "global",
|
|
1674
|
+
key: "description_tag",
|
|
1675
|
+
type: "single_line_text_field",
|
|
1676
|
+
value: flags["seo-desc"]
|
|
1677
|
+
});
|
|
1678
|
+
updatedFields.push("seo_desc");
|
|
1679
|
+
}
|
|
1680
|
+
if (metafields.length > 0) {
|
|
1681
|
+
page.metafields = metafields;
|
|
1682
|
+
}
|
|
1683
|
+
if (updatedFields.length === 0) {
|
|
1684
|
+
formatError("No update flags provided; at least one of --title, --body, --body-file, --seo-title, --seo-desc is required");
|
|
1685
|
+
process.exitCode = 2;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
try {
|
|
1689
|
+
const client = getClient(flags);
|
|
1690
|
+
const lookup = await client.query(PAGE_LOOKUP_QUERY, { query: `handle:${handle}` });
|
|
1691
|
+
const foundPage = lookup.pages.edges[0]?.node;
|
|
1692
|
+
if (!foundPage) {
|
|
1693
|
+
formatError(`Page "${handle}" not found`);
|
|
1694
|
+
process.exitCode = 1;
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
const pageId = foundPage.id;
|
|
1698
|
+
const result = await client.query(PAGE_UPDATE_MUTATION, { id: pageId, page });
|
|
1699
|
+
if (result.pageUpdate.userErrors.length > 0) {
|
|
1700
|
+
formatError(result.pageUpdate.userErrors.map((e) => e.message).join("; "));
|
|
1701
|
+
process.exitCode = 1;
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (flags.json) {
|
|
1705
|
+
formatOutput(updatedFields, [], { json: true, noColor: flags.noColor });
|
|
1706
|
+
} else {
|
|
1707
|
+
process.stdout.write(`Updated fields: ${updatedFields.join(", ")}
|
|
1708
|
+
`);
|
|
1709
|
+
}
|
|
1710
|
+
} catch (err) {
|
|
1711
|
+
handleCommandError(err);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
register("page", "Page management", "update", {
|
|
1715
|
+
description: "Update a static store page",
|
|
1716
|
+
handler: handlePageUpdate
|
|
1717
|
+
});
|
|
1718
|
+
var PAGE_SUMMARY_BY_ID_QUERY = `query PageSummary($id: ID!) {
|
|
1719
|
+
page(id: $id) {
|
|
1720
|
+
id
|
|
1721
|
+
title
|
|
1722
|
+
}
|
|
1723
|
+
}`;
|
|
1724
|
+
var PAGE_SUMMARY_BY_HANDLE_QUERY = `query PageSummaryByHandle($query: String!) {
|
|
1725
|
+
pages(first: 1, query: $query) {
|
|
1726
|
+
edges {
|
|
1727
|
+
node {
|
|
1728
|
+
id
|
|
1729
|
+
title
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}`;
|
|
1734
|
+
var PAGE_DELETE_MUTATION = `mutation PageDelete($id: ID!) {
|
|
1735
|
+
pageDelete(id: $id) {
|
|
1736
|
+
deletedPageId
|
|
1737
|
+
userErrors { field message }
|
|
1738
|
+
}
|
|
1739
|
+
}`;
|
|
1740
|
+
async function handlePageDelete(parsed) {
|
|
1741
|
+
const idOrHandle = parsed.args.join(" ");
|
|
1742
|
+
if (!idOrHandle) {
|
|
1743
|
+
formatError("Usage: shopctl page delete <id-or-handle> [--yes]");
|
|
1744
|
+
process.exitCode = 2;
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
try {
|
|
1748
|
+
const client = getClient(parsed.flags);
|
|
1749
|
+
const resolved = resolvePageId(idOrHandle);
|
|
1750
|
+
let pageGid;
|
|
1751
|
+
let pageTitle;
|
|
1752
|
+
if (resolved.type === "gid") {
|
|
1753
|
+
const result2 = await client.query(PAGE_SUMMARY_BY_ID_QUERY, { id: resolved.id });
|
|
1754
|
+
if (!result2.page) {
|
|
1755
|
+
formatError(`Page "${idOrHandle}" not found`);
|
|
1756
|
+
process.exitCode = 1;
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
pageGid = result2.page.id;
|
|
1760
|
+
pageTitle = result2.page.title;
|
|
1761
|
+
} else {
|
|
1762
|
+
const result2 = await client.query(PAGE_SUMMARY_BY_HANDLE_QUERY, { query: `handle:${resolved.handle}` });
|
|
1763
|
+
const found = result2.pages.edges[0]?.node;
|
|
1764
|
+
if (!found) {
|
|
1765
|
+
formatError(`Page "${idOrHandle}" not found`);
|
|
1766
|
+
process.exitCode = 1;
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
pageGid = found.id;
|
|
1770
|
+
pageTitle = found.title;
|
|
1771
|
+
}
|
|
1772
|
+
if (!parsed.flags.yes) {
|
|
1773
|
+
const data2 = { id: pageGid, title: pageTitle };
|
|
1774
|
+
if (parsed.flags.json) {
|
|
1775
|
+
formatOutput(data2, [], { json: true, noColor: parsed.flags.noColor });
|
|
1776
|
+
} else {
|
|
1777
|
+
process.stdout.write(`Would delete page: ${pageTitle} (${pageGid})
|
|
1778
|
+
`);
|
|
1779
|
+
}
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
const result = await client.query(PAGE_DELETE_MUTATION, { id: pageGid });
|
|
1783
|
+
if (result.pageDelete.userErrors.length > 0) {
|
|
1784
|
+
formatError(result.pageDelete.userErrors.map((e) => e.message).join("; "));
|
|
1785
|
+
process.exitCode = 1;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
const data = { id: pageGid, title: pageTitle };
|
|
1789
|
+
if (parsed.flags.json) {
|
|
1790
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
1791
|
+
} else {
|
|
1792
|
+
process.stdout.write(`Deleted page: ${pageTitle} (${pageGid})
|
|
1793
|
+
`);
|
|
1794
|
+
}
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
handleCommandError(err);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
register("page", "Page management", "delete", {
|
|
1800
|
+
description: "Delete a page by ID or handle",
|
|
1801
|
+
handler: handlePageDelete
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
// src/commands/collection.ts
|
|
1805
|
+
var COLLECTION_GET_BY_ID_QUERY = `query CollectionGet($id: ID!) {
|
|
1806
|
+
collection(id: $id) {
|
|
1807
|
+
id
|
|
1808
|
+
title
|
|
1809
|
+
handle
|
|
1810
|
+
descriptionHtml
|
|
1811
|
+
productsCount { count precision }
|
|
1812
|
+
image { url altText }
|
|
1813
|
+
seo { title description }
|
|
1814
|
+
}
|
|
1815
|
+
}`;
|
|
1816
|
+
var COLLECTION_GET_BY_HANDLE_QUERY = `query CollectionGetByHandle($handle: String!) {
|
|
1817
|
+
collectionByHandle(handle: $handle) {
|
|
1818
|
+
id
|
|
1819
|
+
title
|
|
1820
|
+
handle
|
|
1821
|
+
descriptionHtml
|
|
1822
|
+
productsCount { count precision }
|
|
1823
|
+
image { url altText }
|
|
1824
|
+
seo { title description }
|
|
1825
|
+
}
|
|
1826
|
+
}`;
|
|
1827
|
+
function resolveCollectionInput(input) {
|
|
1828
|
+
if (input.startsWith("gid://")) {
|
|
1829
|
+
return { type: "gid", id: input };
|
|
1830
|
+
}
|
|
1831
|
+
if (/^\d+$/.test(input)) {
|
|
1832
|
+
return { type: "gid", id: `gid://shopify/Collection/${input}` };
|
|
1833
|
+
}
|
|
1834
|
+
return { type: "handle", handle: input };
|
|
1835
|
+
}
|
|
1836
|
+
function stripHtml2(html) {
|
|
1837
|
+
return html.replace(/<[^>]*>/g, "").trim();
|
|
1838
|
+
}
|
|
1839
|
+
function truncate2(str, max) {
|
|
1840
|
+
if (str.length <= max)
|
|
1841
|
+
return str;
|
|
1842
|
+
return `${str.slice(0, max - 3)}...`;
|
|
1843
|
+
}
|
|
1844
|
+
async function handleCollectionGet(parsed) {
|
|
1845
|
+
const idOrHandle = parsed.args.join(" ");
|
|
1846
|
+
if (!idOrHandle) {
|
|
1847
|
+
formatError("Usage: shopctl collection get <id-or-handle>");
|
|
1848
|
+
process.exitCode = 2;
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
try {
|
|
1852
|
+
const client = getClient(parsed.flags);
|
|
1853
|
+
const resolved = resolveCollectionInput(idOrHandle);
|
|
1854
|
+
let collection = null;
|
|
1855
|
+
if (resolved.type === "gid") {
|
|
1856
|
+
const result = await client.query(COLLECTION_GET_BY_ID_QUERY, { id: resolved.id });
|
|
1857
|
+
collection = result.collection;
|
|
1858
|
+
} else {
|
|
1859
|
+
const result = await client.query(COLLECTION_GET_BY_HANDLE_QUERY, { handle: resolved.handle });
|
|
1860
|
+
collection = result.collectionByHandle;
|
|
1861
|
+
}
|
|
1862
|
+
if (!collection) {
|
|
1863
|
+
formatError(`Collection "${idOrHandle}" not found`);
|
|
1864
|
+
process.exitCode = 1;
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (parsed.flags.json) {
|
|
1868
|
+
const data = {
|
|
1869
|
+
id: collection.id,
|
|
1870
|
+
title: collection.title,
|
|
1871
|
+
handle: collection.handle,
|
|
1872
|
+
description: stripHtml2(collection.descriptionHtml),
|
|
1873
|
+
productsCount: collection.productsCount,
|
|
1874
|
+
image: collection.image ? { url: collection.image.url, alt: collection.image.altText ?? "" } : null,
|
|
1875
|
+
seo: collection.seo
|
|
1876
|
+
};
|
|
1877
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
1881
|
+
const lines = [];
|
|
1882
|
+
lines.push(`${label("ID")}: ${collection.id}`);
|
|
1883
|
+
lines.push(`${label("Title")}: ${collection.title}`);
|
|
1884
|
+
lines.push(`${label("Handle")}: ${collection.handle}`);
|
|
1885
|
+
lines.push(`${label("Products Count")}: ${collection.productsCount.count}`);
|
|
1886
|
+
lines.push(`${label("Description")}: ${truncate2(stripHtml2(collection.descriptionHtml), 80)}`);
|
|
1887
|
+
if (collection.image) {
|
|
1888
|
+
lines.push(`${label("Image")}: ${collection.image.url}${collection.image.altText ? ` (${collection.image.altText})` : ""}`);
|
|
1889
|
+
}
|
|
1890
|
+
lines.push(`${label("SEO Title")}: ${collection.seo.title}`);
|
|
1891
|
+
lines.push(`${label("SEO Description")}: ${collection.seo.description}`);
|
|
1892
|
+
process.stdout.write(`${lines.join(`
|
|
1893
|
+
`)}
|
|
1894
|
+
`);
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
handleCommandError(err);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
var COLLECTIONS_QUERY = `query CollectionList($first: Int!, $after: String) {
|
|
1900
|
+
collections(first: $first, after: $after) {
|
|
1901
|
+
edges {
|
|
1902
|
+
node {
|
|
1903
|
+
id
|
|
1904
|
+
title
|
|
1905
|
+
handle
|
|
1906
|
+
descriptionHtml
|
|
1907
|
+
productsCount { count precision }
|
|
1908
|
+
image { url altText }
|
|
1909
|
+
seo { title description }
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
pageInfo { hasNextPage endCursor }
|
|
1913
|
+
}
|
|
1914
|
+
}`;
|
|
1915
|
+
async function handleCollectionList(parsed) {
|
|
1916
|
+
try {
|
|
1917
|
+
const client = getClient(parsed.flags);
|
|
1918
|
+
const limit = clampLimit(parsed.flags.limit);
|
|
1919
|
+
const variables = { first: limit };
|
|
1920
|
+
if (parsed.flags.cursor) {
|
|
1921
|
+
variables.after = parsed.flags.cursor;
|
|
1922
|
+
}
|
|
1923
|
+
const result = await client.query(COLLECTIONS_QUERY, variables);
|
|
1924
|
+
const collections = result.collections.edges.map((e) => ({
|
|
1925
|
+
id: e.node.id,
|
|
1926
|
+
title: e.node.title,
|
|
1927
|
+
handle: e.node.handle,
|
|
1928
|
+
description: stripHtml2(e.node.descriptionHtml),
|
|
1929
|
+
productsCount: parsed.flags.json ? e.node.productsCount : e.node.productsCount.count,
|
|
1930
|
+
image: e.node.image ? { url: e.node.image.url, alt: e.node.image.altText ?? "" } : null,
|
|
1931
|
+
seo: e.node.seo
|
|
1932
|
+
}));
|
|
1933
|
+
const pageInfo = result.collections.pageInfo;
|
|
1934
|
+
if (parsed.flags.json) {
|
|
1935
|
+
formatOutput(collections, [], {
|
|
1936
|
+
json: true,
|
|
1937
|
+
noColor: parsed.flags.noColor,
|
|
1938
|
+
pageInfo
|
|
1939
|
+
});
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const columns = [
|
|
1943
|
+
{ key: "id", header: "ID" },
|
|
1944
|
+
{ key: "title", header: "Title" },
|
|
1945
|
+
{ key: "handle", header: "Handle" },
|
|
1946
|
+
{ key: "productsCount", header: "Products" },
|
|
1947
|
+
{ key: "image", header: "Image", format: (v) => v ? "Yes" : "No" },
|
|
1948
|
+
{ key: "description", header: "Description" }
|
|
1949
|
+
];
|
|
1950
|
+
formatOutput(collections, columns, {
|
|
1951
|
+
json: false,
|
|
1952
|
+
noColor: parsed.flags.noColor,
|
|
1953
|
+
pageInfo
|
|
1954
|
+
});
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
handleCommandError(err);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
register("collection", "Collection management", "list", {
|
|
1960
|
+
description: "List collections with pagination",
|
|
1961
|
+
handler: handleCollectionList
|
|
1962
|
+
});
|
|
1963
|
+
register("collection", "Collection management", "get", {
|
|
1964
|
+
description: "Get a single collection by ID or handle",
|
|
1965
|
+
handler: handleCollectionGet
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
// src/commands/theme.ts
|
|
1969
|
+
var THEMES_LIST_QUERY = `query ThemeList {
|
|
1970
|
+
themes(first: 250) {
|
|
1971
|
+
edges {
|
|
1972
|
+
node {
|
|
1973
|
+
id
|
|
1974
|
+
name
|
|
1975
|
+
role
|
|
1976
|
+
createdAt
|
|
1977
|
+
updatedAt
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}`;
|
|
1982
|
+
function extractNumericId(gid) {
|
|
1983
|
+
const match = gid.match(/(\d+)$/);
|
|
1984
|
+
return match ? match[1] : gid;
|
|
1985
|
+
}
|
|
1986
|
+
async function handleThemeList(parsed) {
|
|
1987
|
+
try {
|
|
1988
|
+
const client = getClient(parsed.flags);
|
|
1989
|
+
const result = await client.query(THEMES_LIST_QUERY, {});
|
|
1990
|
+
const themes = result.themes.edges.map((e) => e.node);
|
|
1991
|
+
if (parsed.flags.json) {
|
|
1992
|
+
const data = themes.map((t) => ({
|
|
1993
|
+
id: t.id,
|
|
1994
|
+
numericId: extractNumericId(t.id),
|
|
1995
|
+
name: t.name,
|
|
1996
|
+
role: t.role,
|
|
1997
|
+
createdAt: t.createdAt,
|
|
1998
|
+
updatedAt: t.updatedAt
|
|
1999
|
+
}));
|
|
2000
|
+
formatOutput(data, [], { json: true, noColor: parsed.flags.noColor });
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
const label = (name) => parsed.flags.noColor ? name : `\x1B[1m${name}\x1B[0m`;
|
|
2004
|
+
const lines = [];
|
|
2005
|
+
for (let i = 0;i < themes.length; i++) {
|
|
2006
|
+
const theme = themes[i];
|
|
2007
|
+
if (i > 0)
|
|
2008
|
+
lines.push("");
|
|
2009
|
+
lines.push(`${label("ID")}: ${theme.id}`);
|
|
2010
|
+
lines.push(`${label("Numeric ID")}: ${extractNumericId(theme.id)}`);
|
|
2011
|
+
lines.push(`${label("Name")}: ${theme.name}`);
|
|
2012
|
+
lines.push(`${label("Role")}: ${theme.role}`);
|
|
2013
|
+
lines.push(`${label("Created")}: ${theme.createdAt}`);
|
|
2014
|
+
lines.push(`${label("Updated")}: ${theme.updatedAt}`);
|
|
2015
|
+
}
|
|
2016
|
+
process.stdout.write(`${lines.join(`
|
|
2017
|
+
`)}
|
|
2018
|
+
`);
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
handleCommandError(err);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
register("theme", "Theme management", "list", {
|
|
2024
|
+
description: "List all themes",
|
|
2025
|
+
handler: handleThemeList
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
// src/cli.ts
|
|
2029
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2030
|
+
|
|
2031
|
+
// src/help.ts
|
|
2032
|
+
function topLevelHelp() {
|
|
2033
|
+
const lines = [
|
|
2034
|
+
"Usage: shopctl <resource> <verb> [args] [flags]",
|
|
2035
|
+
"",
|
|
2036
|
+
"Global Flags:",
|
|
2037
|
+
" --json, -j Output as JSON",
|
|
2038
|
+
" --help, -h Show help",
|
|
2039
|
+
" --version, -v Print version",
|
|
2040
|
+
" --store <url> Store override",
|
|
2041
|
+
" --no-color Disable colored output (also respects NO_COLOR env)",
|
|
2042
|
+
""
|
|
2043
|
+
];
|
|
2044
|
+
const resources2 = getAllResources();
|
|
2045
|
+
if (resources2.size > 0) {
|
|
2046
|
+
lines.push("Resources:");
|
|
2047
|
+
for (const [name, res] of resources2) {
|
|
2048
|
+
lines.push(` ${name.padEnd(16)} ${res.description}`);
|
|
2049
|
+
}
|
|
2050
|
+
lines.push("");
|
|
2051
|
+
}
|
|
2052
|
+
return lines.join(`
|
|
2053
|
+
`);
|
|
2054
|
+
}
|
|
2055
|
+
function resourceHelp(resourceName) {
|
|
2056
|
+
const resource = getResource(resourceName);
|
|
2057
|
+
if (!resource)
|
|
2058
|
+
return;
|
|
2059
|
+
const lines = [
|
|
2060
|
+
`Usage: shopctl ${resourceName} <verb> [args] [flags]`,
|
|
2061
|
+
"",
|
|
2062
|
+
`${resource.description}`,
|
|
2063
|
+
"",
|
|
2064
|
+
"Verbs:"
|
|
2065
|
+
];
|
|
2066
|
+
for (const [verb, cmd] of resource.verbs) {
|
|
2067
|
+
lines.push(` ${verb.padEnd(16)} ${cmd.description}`);
|
|
2068
|
+
}
|
|
2069
|
+
lines.push("");
|
|
2070
|
+
return lines.join(`
|
|
2071
|
+
`);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/parse.ts
|
|
2075
|
+
function parseArgs(argv) {
|
|
2076
|
+
const flags = {
|
|
2077
|
+
json: false,
|
|
2078
|
+
help: false,
|
|
2079
|
+
version: false,
|
|
2080
|
+
noColor: "NO_COLOR" in process.env
|
|
2081
|
+
};
|
|
2082
|
+
const positional = [];
|
|
2083
|
+
let i = 0;
|
|
2084
|
+
while (i < argv.length) {
|
|
2085
|
+
const arg = argv[i];
|
|
2086
|
+
if (arg === "--version" || arg === "-v") {
|
|
2087
|
+
flags.version = true;
|
|
2088
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
2089
|
+
flags.help = true;
|
|
2090
|
+
} else if (arg === "--json" || arg === "-j") {
|
|
2091
|
+
flags.json = true;
|
|
2092
|
+
} else if (arg === "--no-color") {
|
|
2093
|
+
flags.noColor = true;
|
|
2094
|
+
} else if (arg === "--store") {
|
|
2095
|
+
i++;
|
|
2096
|
+
flags.store = argv[i];
|
|
2097
|
+
} else if (arg?.startsWith("--store=")) {
|
|
2098
|
+
flags.store = arg.slice("--store=".length);
|
|
2099
|
+
} else if (arg === "--vars") {
|
|
2100
|
+
i++;
|
|
2101
|
+
flags.vars = argv[i];
|
|
2102
|
+
} else if (arg?.startsWith("--vars=")) {
|
|
2103
|
+
flags.vars = arg.slice("--vars=".length);
|
|
2104
|
+
} else if (arg === "--file") {
|
|
2105
|
+
i++;
|
|
2106
|
+
flags.file = argv[i];
|
|
2107
|
+
} else if (arg?.startsWith("--file=")) {
|
|
2108
|
+
flags.file = arg.slice("--file=".length);
|
|
2109
|
+
} else if (arg === "--status") {
|
|
2110
|
+
i++;
|
|
2111
|
+
flags.status = argv[i];
|
|
2112
|
+
} else if (arg?.startsWith("--status=")) {
|
|
2113
|
+
flags.status = arg.slice("--status=".length);
|
|
2114
|
+
} else if (arg === "--type") {
|
|
2115
|
+
i++;
|
|
2116
|
+
flags.type = argv[i];
|
|
2117
|
+
} else if (arg?.startsWith("--type=")) {
|
|
2118
|
+
flags.type = arg.slice("--type=".length);
|
|
2119
|
+
} else if (arg === "--vendor") {
|
|
2120
|
+
i++;
|
|
2121
|
+
flags.vendor = argv[i];
|
|
2122
|
+
} else if (arg?.startsWith("--vendor=")) {
|
|
2123
|
+
flags.vendor = arg.slice("--vendor=".length);
|
|
2124
|
+
} else if (arg === "--limit") {
|
|
2125
|
+
i++;
|
|
2126
|
+
flags.limit = argv[i];
|
|
2127
|
+
} else if (arg?.startsWith("--limit=")) {
|
|
2128
|
+
flags.limit = arg.slice("--limit=".length);
|
|
2129
|
+
} else if (arg === "--cursor") {
|
|
2130
|
+
i++;
|
|
2131
|
+
flags.cursor = argv[i];
|
|
2132
|
+
} else if (arg?.startsWith("--cursor=")) {
|
|
2133
|
+
flags.cursor = arg.slice("--cursor=".length);
|
|
2134
|
+
} else if (arg === "--title") {
|
|
2135
|
+
i++;
|
|
2136
|
+
flags.title = argv[i];
|
|
2137
|
+
} else if (arg?.startsWith("--title=")) {
|
|
2138
|
+
flags.title = arg.slice("--title=".length);
|
|
2139
|
+
} else if (arg === "--handle") {
|
|
2140
|
+
i++;
|
|
2141
|
+
flags.handle = argv[i];
|
|
2142
|
+
} else if (arg?.startsWith("--handle=")) {
|
|
2143
|
+
flags.handle = arg.slice("--handle=".length);
|
|
2144
|
+
} else if (arg === "--tags") {
|
|
2145
|
+
i++;
|
|
2146
|
+
flags.tags = argv[i];
|
|
2147
|
+
} else if (arg?.startsWith("--tags=")) {
|
|
2148
|
+
flags.tags = arg.slice("--tags=".length);
|
|
2149
|
+
} else if (arg === "--description") {
|
|
2150
|
+
i++;
|
|
2151
|
+
flags.description = argv[i];
|
|
2152
|
+
} else if (arg?.startsWith("--description=")) {
|
|
2153
|
+
flags.description = arg.slice("--description=".length);
|
|
2154
|
+
} else if (arg === "--variants") {
|
|
2155
|
+
i++;
|
|
2156
|
+
flags.variants = argv[i];
|
|
2157
|
+
} else if (arg?.startsWith("--variants=")) {
|
|
2158
|
+
flags.variants = arg.slice("--variants=".length);
|
|
2159
|
+
} else if (arg === "--options") {
|
|
2160
|
+
i++;
|
|
2161
|
+
flags.options = argv[i];
|
|
2162
|
+
} else if (arg?.startsWith("--options=")) {
|
|
2163
|
+
flags.options = arg.slice("--options=".length);
|
|
2164
|
+
} else if (arg === "--body") {
|
|
2165
|
+
i++;
|
|
2166
|
+
flags.body = argv[i];
|
|
2167
|
+
} else if (arg?.startsWith("--body=")) {
|
|
2168
|
+
flags.body = arg.slice("--body=".length);
|
|
2169
|
+
} else if (arg === "--body-file") {
|
|
2170
|
+
i++;
|
|
2171
|
+
flags["body-file"] = argv[i];
|
|
2172
|
+
} else if (arg?.startsWith("--body-file=")) {
|
|
2173
|
+
flags["body-file"] = arg.slice("--body-file=".length);
|
|
2174
|
+
} else if (arg === "--published") {
|
|
2175
|
+
i++;
|
|
2176
|
+
flags.published = argv[i];
|
|
2177
|
+
} else if (arg?.startsWith("--published=")) {
|
|
2178
|
+
flags.published = arg.slice("--published=".length);
|
|
2179
|
+
} else if (arg === "--seo-title") {
|
|
2180
|
+
i++;
|
|
2181
|
+
flags["seo-title"] = argv[i];
|
|
2182
|
+
} else if (arg?.startsWith("--seo-title=")) {
|
|
2183
|
+
flags["seo-title"] = arg.slice("--seo-title=".length);
|
|
2184
|
+
} else if (arg === "--seo-desc") {
|
|
2185
|
+
i++;
|
|
2186
|
+
flags["seo-desc"] = argv[i];
|
|
2187
|
+
} else if (arg?.startsWith("--seo-desc=")) {
|
|
2188
|
+
flags["seo-desc"] = arg.slice("--seo-desc=".length);
|
|
2189
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
2190
|
+
flags.yes = true;
|
|
2191
|
+
} else if (arg) {
|
|
2192
|
+
positional.push(arg);
|
|
2193
|
+
}
|
|
2194
|
+
i++;
|
|
2195
|
+
}
|
|
2196
|
+
return {
|
|
2197
|
+
resource: positional[0],
|
|
2198
|
+
verb: positional[1],
|
|
2199
|
+
args: positional.slice(2),
|
|
2200
|
+
flags
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/cli.ts
|
|
2205
|
+
var pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
2206
|
+
var pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
2207
|
+
async function run(argv) {
|
|
2208
|
+
const parsed = parseArgs(argv);
|
|
2209
|
+
if (parsed.flags.version) {
|
|
2210
|
+
console.log(pkg.version);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (!parsed.resource || parsed.flags.help && !parsed.resource) {
|
|
2214
|
+
console.log(topLevelHelp());
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const resource = getResource(parsed.resource);
|
|
2218
|
+
if (parsed.flags.help && parsed.resource) {
|
|
2219
|
+
const help = resourceHelp(parsed.resource);
|
|
2220
|
+
if (help) {
|
|
2221
|
+
console.log(help);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
process.stderr.write(`Error: unknown resource "${parsed.resource}"
|
|
2225
|
+
`);
|
|
2226
|
+
process.exitCode = 2;
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
if (!resource) {
|
|
2230
|
+
process.stderr.write(`Error: unknown resource "${parsed.resource}"
|
|
2231
|
+
`);
|
|
2232
|
+
process.exitCode = 2;
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (!parsed.verb) {
|
|
2236
|
+
const defaultCommand2 = resource.verbs.get("_default");
|
|
2237
|
+
if (defaultCommand2) {
|
|
2238
|
+
await defaultCommand2.handler(parsed);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
const help = resourceHelp(parsed.resource);
|
|
2242
|
+
if (help)
|
|
2243
|
+
console.log(help);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const defaultCommand = resource.verbs.get("_default");
|
|
2247
|
+
if (defaultCommand) {
|
|
2248
|
+
parsed.args = [parsed.verb, ...parsed.args];
|
|
2249
|
+
parsed.verb = undefined;
|
|
2250
|
+
await defaultCommand.handler(parsed);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const command = resource.verbs.get(parsed.verb);
|
|
2254
|
+
if (!command) {
|
|
2255
|
+
process.stderr.write(`Error: unknown verb "${parsed.verb}" for resource "${parsed.resource}"
|
|
2256
|
+
`);
|
|
2257
|
+
process.exitCode = 2;
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
await command.handler(parsed);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// bin/shopctl.ts
|
|
2264
|
+
await run(process.argv.slice(2));
|