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.
@@ -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));