shopify-agentic-dev-workflow 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.
Files changed (44) hide show
  1. package/.cursor/rules/shopify-agentic-dev.mdc +290 -0
  2. package/AGENTS.md +285 -0
  3. package/CHANGELOG.md +14 -0
  4. package/CLAUDE.md +285 -0
  5. package/GEMINI.md +285 -0
  6. package/HANDOFF.md +351 -0
  7. package/LICENSE +21 -0
  8. package/README.md +370 -0
  9. package/bin/shopify-agent.js +2786 -0
  10. package/docs/00-prerequisites.md +88 -0
  11. package/docs/01-onboarding.md +111 -0
  12. package/docs/02-theme-sdlc.md +106 -0
  13. package/docs/03-app-sdlc.md +66 -0
  14. package/docs/04-admin-api-ops.md +58 -0
  15. package/docs/05-codex-prompts.md +37 -0
  16. package/docs/06-security-guardrails.md +47 -0
  17. package/docs/07-github-rollout.md +30 -0
  18. package/docs/08-product-design.md +168 -0
  19. package/docs/09-shopify-cli-4-capabilities.md +48 -0
  20. package/docs/10-field-learnings.md +66 -0
  21. package/package.json +82 -0
  22. package/scripts/bootstrap.sh +35 -0
  23. package/scripts/validate-graphql.js +303 -0
  24. package/templates/.env.example +20 -0
  25. package/templates/codex-config.toml +3 -0
  26. package/templates/github-actions/deploy-theme.yml +32 -0
  27. package/templates/graphql/content/pages-list.graphql +12 -0
  28. package/templates/graphql/content/redirects-list.graphql +9 -0
  29. package/templates/graphql/customers/new-customers.graphql +15 -0
  30. package/templates/graphql/customers/top-spenders.graphql +16 -0
  31. package/templates/graphql/discounts/active-discounts.graphql +27 -0
  32. package/templates/graphql/discounts-list.graphql +40 -0
  33. package/templates/graphql/inventory-audit.graphql +38 -0
  34. package/templates/graphql/metafields/product-metafields.graphql +14 -0
  35. package/templates/graphql/orders/list-open.graphql +30 -0
  36. package/templates/graphql/orders/revenue-summary.graphql +15 -0
  37. package/templates/graphql/products/list.graphql +16 -0
  38. package/templates/graphql/products/low-inventory.graphql +18 -0
  39. package/templates/graphql/products-seo-audit.graphql +14 -0
  40. package/templates/graphql/shop-query.graphql +9 -0
  41. package/templates/graphql/store/full-info.graphql +23 -0
  42. package/templates/graphql/store/webhooks.graphql +16 -0
  43. package/templates/prompts/admin-operation.md +12 -0
  44. package/templates/prompts/theme-task.md +19 -0
@@ -0,0 +1,2786 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const readline = require("readline");
7
+ const { spawnSync } = require("child_process");
8
+
9
+ const root = process.cwd();
10
+ const configDir = path.join(root, ".shopify-agent");
11
+ const profilesDir = path.join(configDir, "profiles");
12
+ const configPath = path.join(configDir, "config.json");
13
+ const activeProfilePath = path.join(configDir, "active-profile");
14
+ const envPath = path.join(root, ".env");
15
+
16
+ const DEFAULT_API_VERSION = "2026-04";
17
+ const DEFAULT_STORE_SCOPES = [
18
+ // Products, variants, collections, metafields on products
19
+ "read_products", "write_products",
20
+ // Orders + cancellations + closures
21
+ "read_orders", "write_orders",
22
+ // Inventory levels + adjustments
23
+ "read_inventory", "write_inventory",
24
+ // Customers + tags
25
+ "read_customers", "write_customers",
26
+ // Discount codes + price rules
27
+ "read_discounts", "write_discounts",
28
+ "read_price_rules", "write_price_rules",
29
+ // Pages, blogs, articles, URL redirects
30
+ "read_content", "write_content",
31
+ // URL redirects and online store navigation
32
+ "read_online_store_navigation", "write_online_store_navigation",
33
+ // Gift cards
34
+ "read_gift_cards", "write_gift_cards",
35
+ // Locations (for inventory:list, store:locations)
36
+ "read_locations",
37
+ // Metaobjects
38
+ "read_metaobjects", "write_metaobjects",
39
+ "read_metaobject_definitions"
40
+ ].join(",");
41
+ const MIN_NODE_MAJOR = 18;
42
+
43
+ const YES_FLAG = process.argv.includes("--yes") || process.argv.includes("-y");
44
+
45
+ // Enforce Node version early so the error is readable, not a syntax crash.
46
+ const nodeMajor = Number.parseInt(process.version.slice(1).split(".")[0], 10);
47
+ if (nodeMajor < MIN_NODE_MAJOR) {
48
+ process.stderr.write(`shopify-agent requires Node.js ${MIN_NODE_MAJOR}+. You have ${process.version}.\nInstall the latest LTS from https://nodejs.org or via: brew install node\n`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const AGENT_FILES = {
53
+ codex: "AGENTS.md",
54
+ claude: "CLAUDE.md",
55
+ cursor: path.join(".cursor", "rules", "shopify-agentic-dev.mdc"),
56
+ gemini: "GEMINI.md"
57
+ };
58
+
59
+ function printHelp() {
60
+ console.log(`
61
+ shopify-agent
62
+
63
+ Commands:
64
+ init Interactive auth, store, theme, and profile setup
65
+ auth Log in with Shopify CLI
66
+ doctor Check local Shopify agent development prerequisites
67
+ capabilities Show detected Shopify CLI capabilities
68
+ mcp:install Add Shopify Dev MCP server to ~/.codex/config.toml
69
+
70
+ agents:install Install Codex/Claude/Cursor/Gemini guardrail files
71
+ profile:list List local Shopify project profiles
72
+ profile:use Switch active profile
73
+ store:list List Shopify organizations and stores linked to your account
74
+ store:auth Authenticate store commands with selected Admin scopes
75
+ theme:list List themes for the active store
76
+ theme:select Select active theme from Shopify theme list
77
+ theme:pull Pull theme files with Shopify CLI
78
+ theme:dev Start Shopify theme dev preview
79
+ theme:check Run Shopify Theme Check
80
+ theme:push Push to configured unpublished/staging theme
81
+ theme:publish Publish configured theme after explicit confirmation
82
+
83
+ app:dev Start Shopify app dev
84
+ app:deploy Deploy Shopify app config and extensions
85
+
86
+ admin:query <file> Execute a read-only GraphQL file
87
+ admin:mutate <file> Execute a mutation (prompts for confirmation)
88
+
89
+ ── Catalog (for developers & catalogers) ──
90
+ products:list List products [--status active|draft|archived] [--limit N]
91
+ products:get Interactive product detail viewer
92
+ products:create Wizard to create a product
93
+ products:publish Set a product to Active (visible)
94
+ products:archive Set a product to Archived
95
+ products:draft Set a product back to Draft
96
+ products:delete Delete a product (with confirmation)
97
+ variants:list List variants for a selected product
98
+ variants:update Update price, SKU, barcode for a variant
99
+ collections:list List all collections
100
+ collections:create Wizard to create a manual or smart collection
101
+
102
+ ── Inventory ──
103
+ inventory:list List all inventory levels across locations
104
+ inventory:set <qty> Set ALL product variants to <qty> (bulk)
105
+ inventory:adjust Adjust a single variant by delta (+5, -3) or absolute (=10)
106
+
107
+ ── Orders (for business admins) ──
108
+ orders:list List orders [--status open|closed|cancelled|any] [--limit N]
109
+ orders:get Full order detail with line items + tracking
110
+ orders:cancel Cancel an open order (choose reason, refund, restock)
111
+ orders:close Mark an order as closed
112
+
113
+ ── Customers ──
114
+ customers:list List customers [--limit N]
115
+ customers:search Search by name, email, phone, or tag
116
+ customers:get Full customer detail with order history
117
+ customers:create Wizard to create a customer
118
+ customers:tags Add or remove tags on a customer
119
+
120
+ ── Discounts & Gift Cards (for marketing teams) ──
121
+ discounts:list List all discount codes with usage stats
122
+ discounts:create Interactive wizard to create a discount code
123
+ discounts:delete Delete a discount code
124
+ discounts:enable Re-activate a disabled discount
125
+ discounts:disable Deactivate a discount without deleting
126
+ gift-cards:list List all gift cards
127
+ gift-cards:create Create a gift card (with optional customer assignment)
128
+
129
+ ── Store ──
130
+ store:info Store name, plan, currency, timezone
131
+ store:locations List locations with fulfillment status
132
+ store:dashboard Snapshot: open orders + low stock + active discounts
133
+ dashboard Alias for store:dashboard
134
+
135
+ ── Content ──
136
+ pages:list List all pages
137
+ pages:create Wizard to create a page
138
+ blogs:list List blogs with recent articles
139
+ articles:list List articles in a blog
140
+ redirects:list List all URL redirects
141
+ redirects:create Create a URL redirect
142
+
143
+ ── Metafields ──
144
+ metafields:list List metafields for any resource (product, customer, order, etc.)
145
+ metafields:set Create or update a metafield on any resource
146
+
147
+ Flags:
148
+ --yes, -y Auto-confirm all prompts (useful for scripting)
149
+ --status <s> Filter by status where supported
150
+ --limit <n> Limit result count where supported
151
+
152
+ Examples:
153
+ npx shopify-agentic-dev-workflow init
154
+ shopify-agent doctor
155
+ shopify-agent agents:install
156
+ node ./bin/shopify-agent.js theme:select
157
+ shopify-agent admin:query templates/graphql/shop-query.graphql
158
+ `);
159
+ }
160
+
161
+ function loadDotEnv() {
162
+ if (!fs.existsSync(envPath)) return {};
163
+ const out = {};
164
+ for (const line of fs.readFileSync(envPath, "utf8").split(/\r?\n/)) {
165
+ const trimmed = line.trim();
166
+ if (!trimmed || trimmed.startsWith("#")) continue;
167
+ const idx = trimmed.indexOf("=");
168
+ if (idx === -1) continue;
169
+ const key = trimmed.slice(0, idx).trim();
170
+ const value = trimmed.slice(idx + 1).trim();
171
+ out[key] = value;
172
+ }
173
+ return out;
174
+ }
175
+
176
+ function loadConfig() {
177
+ const env = { ...loadDotEnv(), ...process.env };
178
+ let file = {};
179
+ if (fs.existsSync(configPath)) {
180
+ file = JSON.parse(fs.readFileSync(configPath, "utf8"));
181
+ }
182
+ const profile = loadActiveProfile();
183
+ return {
184
+ profile: profile.name || file.profile || "default",
185
+ store: env.SHOPIFY_STORE || profile.store || file.store || "",
186
+ themeId: env.SHOPIFY_THEME_ID || profile.themeId || file.themeId || "",
187
+ themeName: profile.themeName || file.themeName || "",
188
+ themeRole: profile.themeRole || file.themeRole || "",
189
+ themeToken: env.SHOPIFY_CLI_THEME_TOKEN || profile.themeToken || "",
190
+ appConfig: env.SHOPIFY_APP_CONFIG || profile.appConfig || file.appConfig || "",
191
+ appClientId: env.SHOPIFY_APP_CLIENT_ID || profile.appClientId || file.appClientId || "",
192
+ storeScopes: env.SHOPIFY_STORE_SCOPES || profile.storeScopes || file.storeScopes || DEFAULT_STORE_SCOPES,
193
+ apiVersion: env.SHOPIFY_API_VERSION || profile.apiVersion || file.apiVersion || DEFAULT_API_VERSION
194
+ };
195
+ }
196
+
197
+ function ensureConfigDir() {
198
+ fs.mkdirSync(configDir, { recursive: true });
199
+ fs.mkdirSync(profilesDir, { recursive: true });
200
+ }
201
+
202
+ function ask(question, defaultValue = "") {
203
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
204
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
205
+ return new Promise((resolve) => {
206
+ rl.question(`${question}${suffix}: `, (answer) => {
207
+ rl.close();
208
+ resolve(answer.trim() || defaultValue);
209
+ });
210
+ });
211
+ }
212
+
213
+ async function confirm(question, defaultValue = false) {
214
+ if (YES_FLAG) { console.log(`${question} → yes (--yes flag)`); return true; }
215
+ const marker = defaultValue ? "Y/n" : "y/N";
216
+ const answer = (await ask(`${question} [${marker}]`, "")).toLowerCase();
217
+ if (!answer) return defaultValue;
218
+ return ["y", "yes"].includes(answer);
219
+ }
220
+
221
+ async function choose(question, choices, labeler = (item) => String(item), defaultIndex = 0) {
222
+ if (!choices.length) return null;
223
+ console.log(`\n${question}`);
224
+ choices.forEach((choice, index) => {
225
+ const marker = index === defaultIndex ? " *" : " ";
226
+ console.log(`${marker} ${index + 1}. ${labeler(choice)}`);
227
+ });
228
+ while (true) {
229
+ const answer = await ask("Select number", String(defaultIndex + 1));
230
+ const index = Number.parseInt(answer, 10) - 1;
231
+ if (index >= 0 && index < choices.length) return choices[index];
232
+ console.log("Invalid selection.");
233
+ }
234
+ }
235
+
236
+ async function multiChoose(question, choices, labeler = (item) => String(item), defaults = []) {
237
+ console.log(`\n${question}`);
238
+ choices.forEach((choice, index) => {
239
+ const selected = defaults.includes(choice) ? "*" : " ";
240
+ console.log(` ${index + 1}. [${selected}] ${labeler(choice)}`);
241
+ });
242
+ const answer = await ask("Select numbers comma-separated, or 'all'", defaults.length ? defaults.map((item) => choices.indexOf(item) + 1).join(",") : "all");
243
+ if (answer.toLowerCase() === "all") return choices;
244
+ return answer
245
+ .split(",")
246
+ .map((part) => Number.parseInt(part.trim(), 10) - 1)
247
+ .filter((index) => index >= 0 && index < choices.length)
248
+ .map((index) => choices[index]);
249
+ }
250
+
251
+ function run(command, args, options = {}) {
252
+ const result = spawnSync(command, args, {
253
+ stdio: options.input ? ["pipe", "inherit", "inherit"] : "inherit",
254
+ input: options.input,
255
+ env: { ...process.env, ...options.env },
256
+ cwd: options.cwd || root
257
+ });
258
+ if (result.error) {
259
+ console.error(result.error.message);
260
+ process.exit(1);
261
+ }
262
+ process.exitCode = result.status || 0;
263
+ return result.status || 0;
264
+ }
265
+
266
+ function runShopify(args, options = {}) {
267
+ run("shopify", args, options);
268
+ }
269
+
270
+ function ensureCleanGitWorktree(actionLabel) {
271
+ const inside = capture("git", ["rev-parse", "--is-inside-work-tree"]);
272
+ if (!inside.ok || inside.stdout.trim() !== "true") {
273
+ console.error(`Git guard blocked ${actionLabel}.`);
274
+ console.error("\nThis command must run inside a Git repository so theme code is version-controlled.");
275
+ console.error("\nRun:");
276
+ console.error(" git init");
277
+ console.error(" git add assets config layout locales sections snippets templates package.json README.md");
278
+ console.error(" git commit -m \"Initial theme snapshot\"");
279
+ process.exit(1);
280
+ }
281
+
282
+ const rootResult = capture("git", ["rev-parse", "--show-toplevel"]);
283
+ const gitRoot = rootResult.ok ? rootResult.stdout.trim() : root;
284
+ const status = capture("git", ["status", "--short"], { cwd: gitRoot });
285
+ if (!status.ok) {
286
+ console.error(`Git guard blocked ${actionLabel}.`);
287
+ console.error(status.output || "Could not read Git status.");
288
+ process.exit(1);
289
+ }
290
+
291
+ if (status.stdout.trim()) {
292
+ console.error(`Git guard blocked ${actionLabel}: uncommitted changes detected.`);
293
+ console.error("\nCommit or stash your current work before pulling/pushing Shopify theme files.");
294
+ console.error("\nCurrent changes:");
295
+ console.error(status.stdout.trim());
296
+ console.error("\nRun:");
297
+ console.error(" git add .");
298
+ console.error(" git commit -m \"Describe the theme change\"");
299
+ console.error("\nOr:");
300
+ console.error(" git stash push -u -m \"WIP before Shopify theme operation\"");
301
+ process.exit(1);
302
+ }
303
+ }
304
+
305
+ function themeDirectorySlug(cfg) {
306
+ const themeName = cfg.themeName || "theme";
307
+ const slug = String(themeName)
308
+ .toLowerCase()
309
+ .replace(/[^a-z0-9]+/g, "-")
310
+ .replace(/^-+|-+$/g, "")
311
+ .slice(0, 60) || "theme";
312
+ return `${slug}-${cfg.themeId || "unknown"}`;
313
+ }
314
+
315
+ function themeDirectory(cfg) {
316
+ if (!cfg.themeId) {
317
+ console.error("Missing SHOPIFY_THEME_ID. Run `shopify-agent theme:select` first.");
318
+ process.exit(1);
319
+ }
320
+ return path.join(root, "themes", themeDirectorySlug(cfg));
321
+ }
322
+
323
+ function themeMetadataPath(themePath) {
324
+ return path.join(themePath, ".shopify-theme.json");
325
+ }
326
+
327
+ function writeThemeMetadata(themePath, cfg) {
328
+ fs.mkdirSync(themePath, { recursive: true });
329
+ const metadata = {
330
+ store: cfg.store,
331
+ themeId: cfg.themeId,
332
+ themeName: cfg.themeName || "",
333
+ role: cfg.themeRole || "",
334
+ pulledAt: new Date().toISOString()
335
+ };
336
+ fs.writeFileSync(themeMetadataPath(themePath), JSON.stringify(metadata, null, 2) + "\n");
337
+ }
338
+
339
+ function assertThemeDirectoryMatches(themePath, cfg) {
340
+ const metadataFile = themeMetadataPath(themePath);
341
+ if (!fs.existsSync(metadataFile)) return;
342
+ const metadata = JSON.parse(fs.readFileSync(metadataFile, "utf8"));
343
+ if (metadata.store && metadata.store !== cfg.store) {
344
+ console.error(`Theme folder belongs to store ${metadata.store}, not ${cfg.store}.`);
345
+ process.exit(1);
346
+ }
347
+ if (metadata.themeId && String(metadata.themeId) !== String(cfg.themeId)) {
348
+ console.error(`Theme folder belongs to theme ${metadata.themeId}, not ${cfg.themeId}.`);
349
+ process.exit(1);
350
+ }
351
+ }
352
+
353
+ function requirePulledThemeDirectory(cfg, actionLabel) {
354
+ const themePath = themeDirectory(cfg);
355
+ if (!fs.existsSync(themePath)) {
356
+ console.error(`${actionLabel} requires a pulled theme folder.`);
357
+ console.error(`\nExpected: ${path.relative(root, themePath)}`);
358
+ console.error("\nRun:");
359
+ console.error(" shopify-agent theme:pull");
360
+ console.error(" git add themes");
361
+ console.error(" git commit -m \"Pull Shopify theme\"");
362
+ process.exit(1);
363
+ }
364
+ assertThemeDirectoryMatches(themePath, cfg);
365
+ return themePath;
366
+ }
367
+
368
+ // Strip ANSI escape codes from a string so JSON.parse works on captured output.
369
+ function stripAnsi(str) {
370
+ // eslint-disable-next-line no-control-regex
371
+ return (str || "").replace(/\x1B\[[0-9;]*[mGKHF]/g, "");
372
+ }
373
+
374
+ function capture(command, args, options = {}) {
375
+ const result = spawnSync(command, args, {
376
+ encoding: "utf8",
377
+ input: options.input,
378
+ env: { FORCE_COLOR: "0", ...process.env, ...options.env },
379
+ cwd: options.cwd || root
380
+ });
381
+ const stdout = stripAnsi(result.stdout || "");
382
+ const stderr = stripAnsi(result.stderr || "");
383
+ return {
384
+ ok: result.status === 0,
385
+ status: result.status || 0,
386
+ stdout,
387
+ stderr,
388
+ output: `${stdout}${stderr}`.trim(),
389
+ error: result.error
390
+ };
391
+ }
392
+
393
+ function captureShopify(args, options = {}) {
394
+ return capture("shopify", args, options);
395
+ }
396
+
397
+ function check(command, args = ["--version"]) {
398
+ const result = capture(command, args);
399
+ if (result.error) return { ok: false, output: result.error.message };
400
+ return { ok: result.ok, output: result.output };
401
+ }
402
+
403
+ function sanitizeProfileName(name) {
404
+ return (name || "default").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "default";
405
+ }
406
+
407
+ function profileFile(name) {
408
+ return path.join(profilesDir, `${sanitizeProfileName(name)}.json`);
409
+ }
410
+
411
+ function listProfiles() {
412
+ if (!fs.existsSync(profilesDir)) return [];
413
+ return fs
414
+ .readdirSync(profilesDir)
415
+ .filter((file) => file.endsWith(".json"))
416
+ .map((file) => {
417
+ const fullPath = path.join(profilesDir, file);
418
+ try {
419
+ return JSON.parse(fs.readFileSync(fullPath, "utf8"));
420
+ } catch {
421
+ return null;
422
+ }
423
+ })
424
+ .filter(Boolean);
425
+ }
426
+
427
+ function loadProfile(name) {
428
+ const file = profileFile(name);
429
+ if (!fs.existsSync(file)) return {};
430
+ return JSON.parse(fs.readFileSync(file, "utf8"));
431
+ }
432
+
433
+ function loadActiveProfileName() {
434
+ if (!fs.existsSync(activeProfilePath)) return "";
435
+ return fs.readFileSync(activeProfilePath, "utf8").trim();
436
+ }
437
+
438
+ function loadActiveProfile() {
439
+ const name = loadActiveProfileName();
440
+ if (!name) return {};
441
+ return loadProfile(name);
442
+ }
443
+
444
+ function saveProfile(profile) {
445
+ ensureConfigDir();
446
+ const name = sanitizeProfileName(profile.name || profile.store || "default");
447
+ const saved = { ...profile, name };
448
+ fs.writeFileSync(profileFile(name), JSON.stringify(saved, null, 2) + "\n");
449
+ fs.writeFileSync(activeProfilePath, `${name}\n`);
450
+ return saved;
451
+ }
452
+
453
+ function writeLocalEnv(profile, themeToken = "") {
454
+ const managed = {
455
+ SHOPIFY_STORE: profile.store || "",
456
+ SHOPIFY_CLI_THEME_TOKEN: themeToken || "",
457
+ SHOPIFY_THEME_ID: profile.themeId || "",
458
+ SHOPIFY_APP_CONFIG: profile.appConfig || "",
459
+ SHOPIFY_APP_CLIENT_ID: profile.appClientId || "",
460
+ SHOPIFY_STORE_SCOPES: profile.storeScopes || DEFAULT_STORE_SCOPES,
461
+ SHOPIFY_API_VERSION: profile.apiVersion || DEFAULT_API_VERSION
462
+ };
463
+
464
+ if (!fs.existsSync(envPath)) {
465
+ const content = Object.entries(managed).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
466
+ fs.writeFileSync(envPath, content);
467
+ return "created";
468
+ }
469
+
470
+ // Merge-update: preserve existing lines and user-added keys, only update managed keys.
471
+ const existing = fs.readFileSync(envPath, "utf8").split(/\r?\n/);
472
+ const updated = new Set();
473
+ const result = existing.map((line) => {
474
+ const trimmed = line.trim();
475
+ if (!trimmed || trimmed.startsWith("#")) return line;
476
+ const idx = trimmed.indexOf("=");
477
+ if (idx === -1) return line;
478
+ const key = trimmed.slice(0, idx).trim();
479
+ if (key in managed) {
480
+ updated.add(key);
481
+ return `${key}=${managed[key]}`;
482
+ }
483
+ return line;
484
+ });
485
+ // Append any managed keys that were not already in the file.
486
+ for (const [key, val] of Object.entries(managed)) {
487
+ if (!updated.has(key)) result.push(`${key}=${val}`);
488
+ }
489
+ fs.writeFileSync(envPath, result.join("\n").trimEnd() + "\n");
490
+ return "updated";
491
+ }
492
+
493
+ function normalizeStoreDomain(input) {
494
+ const value = (input || "").trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
495
+ if (!value) return "";
496
+ return value.includes(".") ? value : `${value}.myshopify.com`;
497
+ }
498
+
499
+ function themeEnv(cfg) {
500
+ const env = {};
501
+ if (cfg.themeToken) env.SHOPIFY_CLI_THEME_TOKEN = cfg.themeToken;
502
+ return env;
503
+ }
504
+
505
+ function parseThemes(raw) {
506
+ const parsed = JSON.parse(raw);
507
+ const themes = Array.isArray(parsed) ? parsed : parsed.themes || parsed.result || [];
508
+ return themes.map((theme) => ({
509
+ id: String(theme.id || theme.theme_id || ""),
510
+ name: theme.name || theme.themeName || "",
511
+ role: theme.role || theme.status || "",
512
+ raw: theme
513
+ }));
514
+ }
515
+
516
+ function fetchThemes(store, env = {}, themeToken = "") {
517
+ const args = ["theme", "list", "--json", "--store", store];
518
+ if (themeToken) args.push("--password", themeToken);
519
+ const result = captureShopify(args, { env });
520
+ if (!result.ok) {
521
+ const hint = themeToken
522
+ ? "Check that your Theme Access token is valid."
523
+ : "Run `shopify auth login` first, or provide a Theme Access token.";
524
+ return {
525
+ ok: false,
526
+ error: `Unable to list themes. ${hint}\n\nCLI output:\n${result.output || "(no output)"}`
527
+ };
528
+ }
529
+ // CLI 4.x may prefix JSON with informational lines — find the first [ or {
530
+ const raw = result.stdout || result.output;
531
+ const jsonStart = Math.min(
532
+ raw.indexOf("[") !== -1 ? raw.indexOf("[") : Number.POSITIVE_INFINITY,
533
+ raw.indexOf("{") !== -1 ? raw.indexOf("{") : Number.POSITIVE_INFINITY
534
+ );
535
+ const jsonStr = jsonStart !== Number.POSITIVE_INFINITY ? raw.slice(jsonStart) : raw;
536
+ try {
537
+ return { ok: true, themes: parseThemes(jsonStr) };
538
+ } catch (error) {
539
+ return {
540
+ ok: false,
541
+ error: `Could not parse theme list JSON: ${error.message}\n\nRaw output:\n${raw}`
542
+ };
543
+ }
544
+ }
545
+
546
+ function getShopifyCommands() {
547
+ const result = captureShopify(["commands"]);
548
+ return result.ok ? result.output : "";
549
+ }
550
+
551
+ function fetchOrganizations() {
552
+ if (!hasShopifyCommand("organization list")) return { ok: false, orgs: [] };
553
+ const result = captureShopify(["organization", "list", "--json"]);
554
+ if (!result.ok) return { ok: false, orgs: [] };
555
+ const raw = result.stdout || result.output;
556
+ const jsonStart = raw.indexOf("[") !== -1 ? raw.indexOf("[") : raw.indexOf("{");
557
+ if (jsonStart === -1) return { ok: false, orgs: [] };
558
+ try {
559
+ const parsed = JSON.parse(raw.slice(jsonStart));
560
+ const orgs = Array.isArray(parsed) ? parsed : parsed.organizations || [];
561
+ return { ok: true, orgs };
562
+ } catch {
563
+ return { ok: false, orgs: [] };
564
+ }
565
+ }
566
+
567
+ const commandCapabilityCache = new Map();
568
+
569
+ function hasShopifyCommand(commandId) {
570
+ if (commandCapabilityCache.has(commandId)) return commandCapabilityCache.get(commandId);
571
+ const help = captureShopify([...commandId.split(" "), "--help"]);
572
+ const ok = help.ok;
573
+ commandCapabilityCache.set(commandId, ok);
574
+ return ok;
575
+ }
576
+
577
+ function operationKind(query) {
578
+ const cleaned = query
579
+ .replace(/#[^\n]*/g, "")
580
+ .replace(/"([^"\\]|\\.)*"/g, "\"\"")
581
+ .replace(/'([^'\\]|\\.)*'/g, "''");
582
+ return /\bmutation\b/i.test(cleaned) ? "mutation" : "query";
583
+ }
584
+
585
+ function agentInstructions(tool) {
586
+ const cursorPrefix = tool === "cursor" ? "---\ndescription: Shopify agentic development guardrails\nalwaysApply: true\n---\n\n" : "";
587
+ return `${cursorPrefix}# Shopify Agentic Dev Workflow — Agent Guardrails
588
+
589
+ This project is managed by \`shopify-agentic-dev-workflow\`.
590
+ All store operations run through the \`shopify-agent\` CLI. Never call Shopify Admin GraphQL directly — always use the commands below.
591
+
592
+ ---
593
+
594
+ ## Hard Rules — Never Bypass These
595
+
596
+ 1. **Never push to a live/published theme without explicit user confirmation.**
597
+ - Before any \`theme:push\` or \`shopify theme push\`, check if the active theme is live (role: live).
598
+ - If it is live: STOP, explain the consequences below, and ask the user to choose:
599
+ a. Push to a NEW staging theme (\`--unpublished\`) — RECOMMENDED
600
+ b. Push to live (CLI requires typing the store domain to confirm)
601
+ - If the user chooses staging: run \`shopify-agent theme:push\` and tell them to preview before publishing.
602
+ - Never silently push to a live theme.
603
+
604
+ 2. **Never publish a theme without explicit user confirmation.**
605
+ - \`shopify-agent theme:publish\` prompts before going live — never skip or auto-approve on the user's behalf.
606
+ - Use \`--yes\` only when the user has already previewed and explicitly approved the theme for publication.
607
+
608
+ 3. **Never run any mutation command without explicit user confirmation.**
609
+ - Every write command (\`inventory:set\`, \`inventory:adjust\`, \`products:create\`, \`products:delete\`,
610
+ \`variants:update\`, \`orders:cancel\`, \`orders:close\`, \`customers:create\`, \`customers:tags\`,
611
+ \`discounts:create\`, \`discounts:delete\`, \`discounts:enable\`, \`discounts:disable\`,
612
+ \`gift-cards:create\`, \`pages:create\`, \`redirects:create\`, \`metafields:set\`,
613
+ \`collections:create\`, \`admin:mutate\`) prompts for confirmation before executing.
614
+ - Always show what will change and which store before asking.
615
+ - Use \`--yes\` only when the user has already reviewed and explicitly approved the operation.
616
+
617
+ 4. **Never commit or print secrets.**
618
+ - Do not read, log, or include \`.env\`, access tokens, client secrets, or session tokens in any output.
619
+ - \`.env\` and \`.shopify-agent/\` are git-ignored — never stage them.
620
+
621
+ 5. **Never mix files between themes or pull into the project root.**
622
+ - Theme code lives under \`themes/<theme-name>-<theme-id>/\`, never directly in the package root.
623
+ - Before \`theme:pull\`, \`theme:push\`, or \`theme:publish\`, verify the active store, theme ID, theme role, and local theme folder metadata.
624
+ - If the Git worktree is dirty, stop and ask the user to commit or stash first. Pulling Theme A into Theme B's folder or branch is a release blocker.
625
+
626
+ ---
627
+
628
+ ## Live Theme Consequences (always explain before any live push)
629
+
630
+ - The storefront updates IMMEDIATELY — customers browsing right now see the new code
631
+ - Liquid errors, broken CSS, or JS issues go live instantly
632
+ - Shopify does NOT auto-revert — rollback requires manually republishing the previous theme
633
+ - A single typo in a section template can render pages blank for real customers
634
+
635
+ ---
636
+
637
+ ## Setup & Diagnostics
638
+
639
+ \`\`\`bash
640
+ shopify-agent init # Full interactive setup wizard (connect store, auth, pick theme)
641
+ shopify-agent doctor # Check tools, show active config + install hints
642
+ shopify-agent capabilities # Detect which Shopify CLI 4.x commands are available
643
+ shopify-agent auth # shopify auth login (opens browser)
644
+ shopify-agent agents:install # Write/update these guardrail files
645
+ shopify-agent mcp:install # Add Shopify Dev MCP to ~/.codex/config.toml
646
+ shopify-agent profile:list # List saved profiles
647
+ shopify-agent profile:use # Switch active profile
648
+ shopify-agent store:list # Show Shopify orgs linked to your account
649
+ shopify-agent store:auth # Create Admin API token with selected scopes
650
+ \`\`\`
651
+
652
+ ---
653
+
654
+ ## Theme Development (Developers)
655
+
656
+ \`\`\`bash
657
+ shopify-agent theme:list # List themes with roles (live vs unpublished)
658
+ shopify-agent theme:select # Pick active theme (live ⚠ warning included)
659
+ shopify-agent theme:pull # Download theme files locally
660
+ shopify-agent theme:dev # Hot-reload dev server — opens browser automatically
661
+ shopify-agent theme:check # Liquid linting via Theme Check
662
+ shopify-agent theme:push # Push to staging (3-option guard if theme is live)
663
+ shopify-agent theme:publish # Prompts for confirmation before going live
664
+ \`\`\`
665
+
666
+ Recommended theme dev workflow:
667
+ 1. \`shopify-agent init\` → select an UNPUBLISHED staging theme
668
+ 2. \`shopify-agent theme:pull\` → download theme files into \`themes/<theme-name>-<theme-id>/\`
669
+ 3. \`shopify-agent theme:dev\` → hot-reload dev server
670
+ 4. Edit Liquid / CSS / JS files locally
671
+ 5. \`shopify-agent theme:check\` → lint after edits
672
+ 6. \`shopify-agent theme:push\` → push to staging theme
673
+ 7. Preview in Shopify Admin → Themes → Preview
674
+ 8. \`shopify-agent theme:publish\` → go live after review
675
+
676
+ If the user asks to push to a live theme, respond:
677
+ > "The configured theme is LIVE. I recommend \`shopify-agent theme:push\` which will offer a safe staging option. Push directly only after you confirm the store domain."
678
+
679
+ ### Theme Folder and Git Discipline
680
+
681
+ - Treat each Shopify theme as its own versioned artifact: \`themes/dawn-169809707321/\`, \`themes/brand-refresh-123456789/\`, etc.
682
+ - A pull must write only into the folder for the selected \`SHOPIFY_THEME_ID\`. If files appear in the repo root, stop and fix the command/path before continuing.
683
+ - Commit after every successful pull and before every push. This gives the team a rollback point and prevents accidental Theme A/Theme B overwrites.
684
+ - Do not switch themes by editing files in place. Run \`shopify-agent theme:select\`, then \`shopify-agent theme:pull\`, and let the CLI create or reuse the matching theme folder.
685
+ - Never remove, rewrite, or bypass \`.shopify-theme.json\` metadata in a theme folder; it is the local proof that the folder belongs to the selected store and theme.
686
+
687
+ ### Figma-to-Dawn Build Rules
688
+
689
+ - Read the Figma file before coding, then map frames to Dawn sections/templates: home, collection/PLP, product/PDP, cart, header, and footer.
690
+ - Build real Shopify Liquid sections with schema settings instead of static HTML where merchants may need control later.
691
+ - Keep design-preview data and live Shopify data separate. Demo store products can break the visual match; add explicit settings such as "Use live collection products" or "Use live product data" before letting catalog data override Figma assets.
692
+ - Make header, menu, product-card, CTA, social, and footer links real and clickable. Fallback links should go to search, collection, product, account, cart, or policy URLs, not dead placeholders.
693
+ - After frontend work, run \`shopify-agent theme:check\` and visually inspect desktop and mobile previews for home, collection, product, header, footer, and horizontal overflow.
694
+
695
+ ---
696
+
697
+ ## App Development (Developers)
698
+
699
+ \`\`\`bash
700
+ shopify-agent app:dev # shopify app dev
701
+ shopify-agent app:deploy # shopify app deploy
702
+ \`\`\`
703
+
704
+ ---
705
+
706
+ ## Raw GraphQL (Developers / Power Users)
707
+
708
+ \`\`\`bash
709
+ shopify-agent admin:query <file.graphql> # Read-only Admin GraphQL
710
+ shopify-agent admin:mutate <file.graphql> # Mutation (always prompts for confirmation)
711
+ \`\`\`
712
+
713
+ Pre-built templates in \`templates/graphql/\`:
714
+ - \`shop-query.graphql\`, \`inventory-audit.graphql\`, \`discounts-list.graphql\`, \`products-seo-audit.graphql\`
715
+ - \`products/list.graphql\`, \`products/low-inventory.graphql\`
716
+ - \`orders/list-open.graphql\`, \`orders/revenue-summary.graphql\`
717
+ - \`customers/top-spenders.graphql\`, \`customers/new-customers.graphql\`
718
+ - \`discounts/active-discounts.graphql\`
719
+ - \`store/full-info.graphql\`, \`store/webhooks.graphql\`
720
+ - \`content/pages-list.graphql\`, \`content/redirects-list.graphql\`
721
+ - \`metafields/product-metafields.graphql\`
722
+
723
+ Schema hygiene:
724
+ - Shopify Admin GraphQL is versioned and fields move or disappear. If a query fails with \`undefinedField\`, treat the template as stale and update it against the configured \`SHOPIFY_API_VERSION\`.
725
+ - Do not assume dashboard, discount, inventory, or product payload shapes from memory. Run the smallest read-only query first, then expand.
726
+ - Keep mutations behind \`admin:mutate\` so the CLI confirmation and store context are visible before anything changes.
727
+
728
+ ---
729
+
730
+ ## Products & Catalog (Catalogers)
731
+
732
+ \`\`\`bash
733
+ shopify-agent products:list [--status active|draft|archived] [--limit N]
734
+ shopify-agent products:get # Interactive full product detail viewer
735
+ shopify-agent products:create # Wizard: title, type, vendor, price, SKU, inventory, status
736
+ shopify-agent products:publish # Set product to Active (visible in store) — prompts
737
+ shopify-agent products:archive # Set product to Archived — prompts
738
+ shopify-agent products:draft # Set product back to Draft — prompts
739
+ shopify-agent products:delete # Delete a product — prompts with confirmation
740
+
741
+ shopify-agent variants:list # Pick a product → see all variants with price/SKU/qty
742
+ shopify-agent variants:update # Update price, SKU, barcode for a variant — prompts
743
+
744
+ shopify-agent collections:list # List all collections (manual + smart)
745
+ shopify-agent collections:create # Wizard: manual or smart (with rule builder) — prompts
746
+ \`\`\`
747
+
748
+ ---
749
+
750
+ ## Inventory (Catalogers / Operations)
751
+
752
+ \`\`\`bash
753
+ shopify-agent inventory:list # List all inventory levels across locations
754
+ shopify-agent inventory:set <qty> # Set ALL variants to <qty> in bulk — prompts
755
+ shopify-agent inventory:adjust # Adjust one variant: +5, -3, or =10 — prompts
756
+ \`\`\`
757
+
758
+ ---
759
+
760
+ ## Orders (Business Admins)
761
+
762
+ \`\`\`bash
763
+ shopify-agent orders:list [--status open|closed|cancelled|any] [--limit N]
764
+ shopify-agent orders:get # Full order detail: line items, tracking, totals
765
+ shopify-agent orders:cancel # Cancel an open order (choose reason, refund, restock) — prompts
766
+ shopify-agent orders:close # Mark an order as closed — prompts
767
+ \`\`\`
768
+
769
+ ---
770
+
771
+ ## Customers (Business Admins / Marketing)
772
+
773
+ \`\`\`bash
774
+ shopify-agent customers:list [--limit N]
775
+ shopify-agent customers:search # Search by name, email, phone, or tag
776
+ shopify-agent customers:get # Full customer profile + recent order history
777
+ shopify-agent customers:create # Wizard: name, email, phone, tags, marketing opt-in — prompts
778
+ shopify-agent customers:tags # Add or remove tags on a customer — prompts
779
+ \`\`\`
780
+
781
+ ---
782
+
783
+ ## Discounts & Gift Cards (Marketing)
784
+
785
+ \`\`\`bash
786
+ shopify-agent discounts:list # List all discount codes with usage stats
787
+ shopify-agent discounts:create # Wizard: % off, fixed amount, per-customer limit, expiry — prompts
788
+ shopify-agent discounts:delete # Delete a discount code — prompts
789
+ shopify-agent discounts:enable # Re-activate a disabled discount — prompts
790
+ shopify-agent discounts:disable # Deactivate without deleting — prompts
791
+
792
+ shopify-agent gift-cards:list # List all gift cards with balance
793
+ shopify-agent gift-cards:create # Create a gift card (with optional customer assignment) — prompts
794
+ \`\`\`
795
+
796
+ ---
797
+
798
+ ## Store Info (Business Admins)
799
+
800
+ \`\`\`bash
801
+ shopify-agent store:info # Name, plan, currency, timezone, multi-currency
802
+ shopify-agent store:locations # List locations with fulfillment status
803
+ shopify-agent store:dashboard # Snapshot: open orders + low-stock + active discounts
804
+ shopify-agent dashboard # Alias for store:dashboard
805
+ \`\`\`
806
+
807
+ ---
808
+
809
+ ## Content (Marketing)
810
+
811
+ \`\`\`bash
812
+ shopify-agent pages:list # List all pages
813
+ shopify-agent pages:create # Wizard: title, body HTML, publish immediately — prompts
814
+ shopify-agent blogs:list # List blogs with 5 recent articles each
815
+ shopify-agent articles:list # Pick a blog → list articles with author + status
816
+ shopify-agent redirects:list # List all URL redirects
817
+ shopify-agent redirects:create # Create a URL redirect (from → to) — prompts
818
+ \`\`\`
819
+
820
+ ---
821
+
822
+ ## Metafields (Developers / Catalogers)
823
+
824
+ \`\`\`bash
825
+ shopify-agent metafields:list # List metafields for any resource
826
+ shopify-agent metafields:set # Create or update a metafield on any resource — prompts
827
+ # Works with: Product, Collection, Customer, Order, Shop
828
+ \`\`\`
829
+
830
+ ---
831
+
832
+ ## Flags
833
+
834
+ \`\`\`bash
835
+ --yes, -y Auto-confirm all prompts (scripting / CI — only use after reviewing the operation)
836
+ --status <s> Filter by status where supported (products:list, orders:list)
837
+ --limit <n> Limit result count where supported
838
+ \`\`\`
839
+
840
+ ---
841
+
842
+ ## Role-Based Workflow Examples
843
+
844
+ **Cataloger — bulk inventory reset before a sale:**
845
+ \`\`\`bash
846
+ shopify-agent inventory:list # review current levels
847
+ shopify-agent inventory:set 50 # bulk-set all variants to 50 (prompts)
848
+ shopify-agent products:list --status active --limit 50 # verify
849
+ \`\`\`
850
+
851
+ **Marketing — launch a discount campaign:**
852
+ \`\`\`bash
853
+ shopify-agent discounts:list # check existing codes
854
+ shopify-agent discounts:create # wizard → 10% off, first order, 7-day expiry (prompts)
855
+ shopify-agent pages:create # create a landing page for the campaign (prompts)
856
+ \`\`\`
857
+
858
+ **Business admin — morning dashboard:**
859
+ \`\`\`bash
860
+ shopify-agent dashboard # open orders + low-stock + active discounts
861
+ shopify-agent orders:list --status open
862
+ shopify-agent customers:list --limit 10
863
+ \`\`\`
864
+
865
+ **Developer — theme deploy pipeline:**
866
+ \`\`\`bash
867
+ shopify-agent theme:check # lint
868
+ shopify-agent theme:push # push to staging
869
+ # preview in Admin
870
+ shopify-agent theme:publish # go live (prompts)
871
+ \`\`\`
872
+ `;
873
+ }
874
+
875
+ async function init() {
876
+ const existing = loadConfig();
877
+ console.log("\nShopify Agent — project setup\n");
878
+
879
+ // Step 1: Auth — `auth whoami` doesn't exist in CLI 4.x; probe with `organization list` instead.
880
+ const authProbe = captureShopify(["organization", "list", "--json"]);
881
+ const alreadyLoggedIn = authProbe.ok;
882
+ if (alreadyLoggedIn) {
883
+ console.log("Already logged in to Shopify CLI.\n");
884
+ } else if (await confirm("Log in with Shopify CLI now (opens browser)", true)) {
885
+ run("shopify", ["auth", "login"]);
886
+ console.log();
887
+ }
888
+
889
+ // Step 2: Profile name
890
+ const profileName = sanitizeProfileName(await ask("Profile name", existing.profile || "default"));
891
+
892
+ // Step 3: Store domain — try org list for hints first
893
+ let suggestedStore = existing.store || "";
894
+ if (!suggestedStore) {
895
+ const { ok, orgs } = fetchOrganizations();
896
+ if (ok && orgs.length) {
897
+ console.log("\nShopify organizations found:");
898
+ orgs.forEach((org, i) => console.log(` ${i + 1}. ${org.businessName || org.name || org.id} (ID: ${org.id})`));
899
+ console.log(" (Store domains are <store>.myshopify.com — check your Shopify Admin or Partners Dashboard)");
900
+ }
901
+ }
902
+ const store = normalizeStoreDomain(await ask("Shopify store domain", suggestedStore || "your-store.myshopify.com"));
903
+
904
+ // Step 4: Auth method
905
+ console.log("\nTheme access: Shopify CLI session (Partners/staff) or Theme Access token.");
906
+ console.log("If you have a Theme Access token (from the Theme Access app), enter it now.");
907
+ console.log("Otherwise leave blank to use your Shopify CLI login session.\n");
908
+ const themeToken = await ask("Theme Access token (optional)", existing.themeToken || "");
909
+ const env = themeToken ? { SHOPIFY_CLI_THEME_TOKEN: themeToken } : {};
910
+
911
+ // Step 5: Theme selection
912
+ let themeId = existing.themeId;
913
+ let themeName = existing.themeName;
914
+ let themeRole = existing.themeRole || "";
915
+ if (await confirm("Fetch themes from this store now", true)) {
916
+ console.log(`\nFetching themes from ${store}...`);
917
+ const result = fetchThemes(store, {}, themeToken);
918
+ if (result.ok && result.themes.length) {
919
+ const unpublished = result.themes.filter((t) => t.role !== "live");
920
+ const liveThemes = result.themes.filter((t) => t.role === "live");
921
+ if (unpublished.length) {
922
+ console.log("Tip: select an UNPUBLISHED theme for safe development. Live themes affect your storefront immediately.\n");
923
+ }
924
+ const existingIndex = Math.max(0, result.themes.findIndex((t) => t.id === existing.themeId));
925
+ const selected = await choose(
926
+ "Select default staging/unpublished theme",
927
+ result.themes,
928
+ (theme) => {
929
+ const liveWarning = theme.role === "live" ? " ⚠ LIVE" : "";
930
+ return `${(theme.name || "(unnamed)").padEnd(40)} ID: ${theme.id.padEnd(14)} role: ${theme.role || "unknown"}${liveWarning}`;
931
+ },
932
+ existingIndex
933
+ );
934
+ if (selected.role === "live") {
935
+ console.log("\n╔══════════════════════════════════════════════════════════╗");
936
+ console.log("║ ⚠ LIVE THEME SELECTED — READ BEFORE CONTINUING ║");
937
+ console.log("╚══════════════════════════════════════════════════════════╝");
938
+ console.log("\nConsequences of working on a live theme:");
939
+ console.log(" • Every `theme:push` will IMMEDIATELY update your public storefront");
940
+ console.log(" • Customers browsing right now will see any broken Liquid or CSS");
941
+ console.log(" • Shopify does NOT auto-revert — you must manually republish the previous theme");
942
+ console.log(" • A single typo in a section template can take your store offline");
943
+ console.log("\nRecommended: pick an UNPUBLISHED theme for development.");
944
+ console.log(" When ready, publish it from Shopify Admin or with `theme:publish`.\n");
945
+ const liveChoice = await choose("What would you like to do?", [
946
+ "switch",
947
+ "live"
948
+ ], (c) => c === "switch"
949
+ ? `Switch to unpublished theme${unpublished[0] ? ` → ${unpublished[0].name}` : " (pick from list)"}`
950
+ : "Continue with LIVE theme anyway (I accept the risk)"
951
+ );
952
+ if (liveChoice === "switch") {
953
+ const fallback = unpublished.length > 1
954
+ ? await choose("Select unpublished theme", unpublished,
955
+ (t) => `${(t.name || "(unnamed)").padEnd(40)} ID: ${t.id}`)
956
+ : unpublished[0] || result.themes[0];
957
+ themeId = fallback.id;
958
+ themeName = fallback.name;
959
+ themeRole = fallback.role || "unpublished";
960
+ console.log(`Switched to: ${themeName} (${themeId})\n`);
961
+ } else {
962
+ themeId = selected.id;
963
+ themeName = selected.name;
964
+ themeRole = "live";
965
+ console.log(`Confirmed live theme: ${themeName} (${themeId}). Every push goes live immediately.\n`);
966
+ }
967
+ } else {
968
+ themeId = selected.id;
969
+ themeName = selected.name;
970
+ themeRole = selected.role || "unpublished";
971
+ console.log(`Selected: ${themeName} (${themeId})\n`);
972
+ }
973
+ } else {
974
+ console.log(`\n${result.error}`);
975
+ console.log("\nTip: Make sure you ran `shopify auth login` and your account has collaborator/staff access.");
976
+ console.log(" Or set a Theme Access token above.\n");
977
+ themeId = await ask("Enter theme ID or name manually (or leave blank)", existing.themeId || "");
978
+ themeName = "";
979
+ themeRole = "";
980
+ }
981
+ } else {
982
+ themeId = await ask("Default staging theme ID/name", existing.themeId || "");
983
+ themeName = "";
984
+ themeRole = "";
985
+ }
986
+
987
+ // Step 6: App and API config
988
+ console.log("\nApp config (only needed for Shopify app development, not theme work).");
989
+ console.log("This is the config key in shopify.app.toml, e.g. 'staging' or 'production' (not a display name).\n");
990
+ const appConfig = await ask("App config key from shopify.app.toml (optional, skip if theme-only)", existing.appConfig || "");
991
+ const appClientId = await ask("Shopify app client ID (optional, skip if theme-only)", existing.appClientId || "");
992
+ const storeScopes = await ask("Admin API scopes for `store auth`", existing.storeScopes || DEFAULT_STORE_SCOPES);
993
+ const apiVersion = await ask("Admin GraphQL API version", existing.apiVersion || DEFAULT_API_VERSION);
994
+
995
+ const profile = saveProfile({ name: profileName, store, themeId, themeName, themeRole, themeToken, appConfig, appClientId, storeScopes, apiVersion });
996
+ fs.writeFileSync(configPath, JSON.stringify({ profile: profile.name }, null, 2) + "\n");
997
+
998
+ const envStatus = writeLocalEnv(profile, themeToken);
999
+ if (envStatus === "created") {
1000
+ console.log("Created .env and .shopify-agent/config.json\n");
1001
+ } else {
1002
+ console.log("Profile saved. .env updated with latest values.\n");
1003
+ }
1004
+
1005
+ // Step 7: Admin API token — required for ALL store commands (dashboard, products, orders, etc.)
1006
+ if (hasShopifyCommand("store auth")) {
1007
+ console.log("─────────────────────────────────────────────────────────────");
1008
+ console.log("Admin API access (required for store commands)");
1009
+ console.log("─────────────────────────────────────────────────────────────");
1010
+ console.log("Commands like dashboard, products:list, orders:list, inventory:set,");
1011
+ console.log("discounts:create and all other store operations need an Admin API token.");
1012
+ console.log("This opens a browser to approve the scopes, then stores the token locally.\n");
1013
+ if (await confirm("Set up Admin API access now (recommended)", true)) {
1014
+ runShopify(["store", "auth", "--store", store, "--scopes", profile.storeScopes || DEFAULT_STORE_SCOPES]);
1015
+ console.log("\nAdmin API token stored. All store commands are now available.\n");
1016
+ } else {
1017
+ console.log("Skipped. Run `shopify-agent store:auth` any time to set this up.\n");
1018
+ }
1019
+ } else {
1020
+ console.log("Note: `shopify store auth` not found in this CLI version.");
1021
+ console.log(" Upgrade with: npm install -g @shopify/cli@latest\n");
1022
+ }
1023
+
1024
+ // Step 8: Agent guardrails
1025
+ if (await confirm("Install agent guardrail files (AGENTS.md / CLAUDE.md etc.) for this project", true)) {
1026
+ await agentsInstall();
1027
+ }
1028
+
1029
+ console.log("\nSetup complete. Next steps:");
1030
+ console.log(` shopify-agent doctor # verify everything is configured`);
1031
+ console.log(` shopify-agent dashboard # open orders, low-stock, active discounts`);
1032
+ console.log(` shopify-agent theme:list # see your themes`);
1033
+ console.log(` shopify-agent theme:pull # download theme files`);
1034
+ console.log(` shopify-agent theme:dev # start hot-reload dev server`);
1035
+ }
1036
+
1037
+ function auth() {
1038
+ run("shopify", ["auth", "login"]);
1039
+ }
1040
+
1041
+ const INSTALL_HINTS = {
1042
+ node: "Install from https://nodejs.org or: brew install node",
1043
+ npm: "Comes with Node.js — reinstall Node from https://nodejs.org",
1044
+ git: "Install from https://git-scm.com or: brew install git",
1045
+ shopify: "Install with: npm install -g @shopify/cli@latest",
1046
+ gh: "Install from https://cli.github.com or: brew install gh (optional)"
1047
+ };
1048
+
1049
+ function doctor() {
1050
+ const cfg = loadConfig();
1051
+ const checks = [
1052
+ ["node", ["--version"]],
1053
+ ["npm", ["--version"]],
1054
+ ["git", ["--version"]],
1055
+ ["shopify", ["version"]],
1056
+ ["gh", ["--version"]]
1057
+ ];
1058
+
1059
+ console.log("Shopify agent development doctor\n");
1060
+ let missingRequired = false;
1061
+ for (const [cmd, args] of checks) {
1062
+ const result = check(cmd, args);
1063
+ if (result.ok) {
1064
+ const firstLine = result.output.split(/\r?\n/)[0] || "";
1065
+ console.log(`OK ${cmd.padEnd(8)} ${firstLine}`);
1066
+ // Extra: check Node version
1067
+ if (cmd === "node") {
1068
+ const major = Number.parseInt((result.output.match(/v(\d+)/) || [])[1] || "0", 10);
1069
+ if (major < MIN_NODE_MAJOR) {
1070
+ console.log(`WARN node Version ${result.output.trim()} is below required ${MIN_NODE_MAJOR}+. Upgrade Node.`);
1071
+ }
1072
+ }
1073
+ } else {
1074
+ const hint = INSTALL_HINTS[cmd] || "";
1075
+ const required = cmd !== "gh";
1076
+ console.log(`MISSING ${cmd.padEnd(8)} ${hint}`);
1077
+ if (required) missingRequired = true;
1078
+ }
1079
+ }
1080
+
1081
+ if (missingRequired) {
1082
+ console.log("\nFix the MISSING tools above, then re-run: shopify-agent doctor");
1083
+ return;
1084
+ }
1085
+
1086
+ console.log("\nConfiguration");
1087
+ console.log(`PROFILE ${cfg.profile || "default"}`);
1088
+ console.log(`STORE ${cfg.store || "(not set — run: shopify-agent init)"}`);
1089
+ console.log(`THEME ${cfg.themeId ? `${cfg.themeId}${cfg.themeName ? ` (${cfg.themeName})` : ""}` : "(not set — run: shopify-agent init)"}`);
1090
+ console.log(`API ${cfg.apiVersion}`);
1091
+ console.log(`THEME AUTH ${cfg.themeToken ? "Theme Access token" : "Shopify CLI login/session"}`);
1092
+ console.log(`APP CONFIG ${cfg.appConfig || "(not set)"}`);
1093
+
1094
+ // Check store execute availability and whether Admin API token exists for this store
1095
+ const storeExecAvailable = hasShopifyCommand("store execute");
1096
+ if (!storeExecAvailable) {
1097
+ console.log(`ADMIN API not available — upgrade: npm install -g @shopify/cli@latest`);
1098
+ } else if (cfg.store) {
1099
+ // Probe: run a minimal query to see if stored auth exists
1100
+ const probe = captureShopify(["store", "execute", "--store", cfg.store, "--query", "{ shop { name } }"]);
1101
+ const noAuth = (probe.stderr || probe.output || "").includes("No stored app authentication");
1102
+ if (noAuth) {
1103
+ console.log(`ADMIN API ✗ not authenticated — run: shopify-agent store:auth`);
1104
+ } else {
1105
+ console.log(`ADMIN API ✓ authenticated (store execute ready)`);
1106
+ }
1107
+ } else {
1108
+ console.log(`ADMIN API available (CLI 4.x) — run init to configure store`);
1109
+ }
1110
+ }
1111
+
1112
+ function capabilities() {
1113
+ const version = check("shopify", ["version"]);
1114
+ console.log(`Shopify CLI: ${version.ok ? version.output.split(/\r?\n/).pop() : "missing"}`);
1115
+ const interesting = [
1116
+ "organization list",
1117
+ "store auth",
1118
+ "store execute",
1119
+ "app execute",
1120
+ "app bulk execute",
1121
+ "app config validate",
1122
+ "theme preview",
1123
+ "theme list",
1124
+ "theme dev",
1125
+ "theme publish"
1126
+ ];
1127
+ for (const commandId of interesting) {
1128
+ console.log(`${hasShopifyCommand(commandId) ? "OK" : "NO"} ${commandId}`);
1129
+ }
1130
+ console.log("\nLatest checked from npm: @shopify/cli@4.0.0 includes organization list, store auth, and store execute.");
1131
+ }
1132
+
1133
+ async function agentsInstall() {
1134
+ ensureConfigDir();
1135
+ const selected = await multiChoose(
1136
+ "Which agent tools should this project support?",
1137
+ Object.keys(AGENT_FILES),
1138
+ (tool) => `${tool} -> ${AGENT_FILES[tool]}`,
1139
+ ["codex"]
1140
+ );
1141
+ if (!selected.length) {
1142
+ console.log("No agent files selected.");
1143
+ return;
1144
+ }
1145
+ for (const tool of selected) {
1146
+ const target = path.join(root, AGENT_FILES[tool]);
1147
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1148
+ if (fs.existsSync(target)) {
1149
+ const shouldOverwrite = await confirm(`${AGENT_FILES[tool]} exists. Overwrite`, false);
1150
+ if (!shouldOverwrite) continue;
1151
+ }
1152
+ fs.writeFileSync(target, agentInstructions(tool));
1153
+ console.log(`Wrote ${AGENT_FILES[tool]}`);
1154
+ }
1155
+ }
1156
+
1157
+ function mcpInstall() {
1158
+ const codexDir = path.join(os.homedir(), ".codex");
1159
+ const codexConfig = path.join(codexDir, "config.toml");
1160
+ const block = [
1161
+ "",
1162
+ "# Shopify Dev MCP: added by shopify-agent",
1163
+ "[mcp_servers.shopify-dev-mcp]",
1164
+ 'command = "npx"',
1165
+ 'args = ["-y", "@shopify/dev-mcp@latest"]',
1166
+ ""
1167
+ ].join("\n");
1168
+
1169
+ fs.mkdirSync(codexDir, { recursive: true });
1170
+ const existing = fs.existsSync(codexConfig) ? fs.readFileSync(codexConfig, "utf8") : "";
1171
+ if (existing.includes("[mcp_servers.shopify-dev-mcp]")) {
1172
+ console.log("Shopify Dev MCP is already configured in ~/.codex/config.toml");
1173
+ return;
1174
+ }
1175
+ fs.writeFileSync(codexConfig, existing.trimEnd() + block);
1176
+ console.log("Added Shopify Dev MCP to ~/.codex/config.toml. Restart Codex to load it.");
1177
+ }
1178
+
1179
+ function requireStore(cfg) {
1180
+ if (!cfg.store) {
1181
+ console.error("Missing SHOPIFY_STORE. Run `npm run setup` first.");
1182
+ process.exit(1);
1183
+ }
1184
+ }
1185
+
1186
+ function shopifyArgsWithStore(cfg, args) {
1187
+ requireStore(cfg);
1188
+ return [...args, "--store", cfg.store];
1189
+ }
1190
+
1191
+ function profileList() {
1192
+ const active = loadActiveProfileName();
1193
+ const profiles = listProfiles();
1194
+ if (!profiles.length) {
1195
+ console.log("No profiles found. Run `npm run setup` first.");
1196
+ return;
1197
+ }
1198
+ for (const profile of profiles) {
1199
+ const marker = profile.name === active ? "*" : " ";
1200
+ console.log(`${marker} ${profile.name} ${profile.store || "-"} ${profile.themeId || "-"}`);
1201
+ }
1202
+ }
1203
+
1204
+ function storeList() {
1205
+ const version = check("shopify", ["version"]);
1206
+ console.log(`Shopify CLI: ${version.ok ? version.output.split(/\r?\n/).pop() : "missing"}\n`);
1207
+ const { ok, orgs } = fetchOrganizations();
1208
+ if (!ok || !orgs.length) {
1209
+ console.log("No organizations found. Make sure you are logged in with `shopify auth login`.");
1210
+ return;
1211
+ }
1212
+ console.log("Organizations:");
1213
+ for (const org of orgs) {
1214
+ console.log(` ${org.id} ${org.businessName || org.name || ""}`);
1215
+ const stores = org.stores || org.devStores || [];
1216
+ for (const s of stores) {
1217
+ console.log(` - ${s.shopDomain || s.domain || s.name || s.id}`);
1218
+ }
1219
+ }
1220
+ console.log("\nTip: Store domain is <handle>.myshopify.com. Check your Partners Dashboard for non-dev stores.");
1221
+ }
1222
+
1223
+ function storeAuth() {
1224
+ const cfg = loadConfig();
1225
+ requireStore(cfg);
1226
+ if (!hasShopifyCommand("store auth")) {
1227
+ console.error("Your Shopify CLI does not support `shopify store auth`. Upgrade with `npm install -g @shopify/cli@latest`.");
1228
+ process.exit(1);
1229
+ }
1230
+ runShopify(["store", "auth", "--store", cfg.store, "--scopes", cfg.storeScopes]);
1231
+ }
1232
+
1233
+ async function profileUse(nameArg) {
1234
+ ensureConfigDir();
1235
+ const profiles = listProfiles();
1236
+ if (!profiles.length) {
1237
+ console.log("No profiles found. Run `npm run setup` first.");
1238
+ return;
1239
+ }
1240
+ let selected = nameArg ? profiles.find((profile) => profile.name === sanitizeProfileName(nameArg)) : null;
1241
+ if (!selected) {
1242
+ selected = await choose("Select profile", profiles, (profile) => `${profile.name} | ${profile.store || "-"}`);
1243
+ }
1244
+ fs.writeFileSync(activeProfilePath, `${selected.name}\n`);
1245
+ fs.writeFileSync(configPath, JSON.stringify({ profile: selected.name }, null, 2) + "\n");
1246
+ console.log(`Active profile: ${selected.name}`);
1247
+ }
1248
+
1249
+ function themeList() {
1250
+ const cfg = loadConfig();
1251
+ requireStore(cfg);
1252
+ const result = fetchThemes(cfg.store, themeEnv(cfg), cfg.themeToken);
1253
+ if (!result.ok) {
1254
+ console.error(result.error);
1255
+ process.exit(1);
1256
+ }
1257
+ for (const theme of result.themes) {
1258
+ console.log(`${theme.id.padEnd(16)} ${String(theme.role || "").padEnd(13)} ${theme.name}`);
1259
+ }
1260
+ }
1261
+
1262
+ async function themeSelect() {
1263
+ const cfg = loadConfig();
1264
+ requireStore(cfg);
1265
+ const result = fetchThemes(cfg.store, themeEnv(cfg), cfg.themeToken);
1266
+ if (!result.ok) {
1267
+ console.error(result.error);
1268
+ process.exit(1);
1269
+ }
1270
+ console.log("Tip: choose an UNPUBLISHED theme for safe development.\n");
1271
+ const currentIndex = Math.max(0, result.themes.findIndex((t) => t.id === cfg.themeId));
1272
+ const selected = await choose(
1273
+ "Select active theme",
1274
+ result.themes,
1275
+ (theme) => {
1276
+ const liveWarning = theme.role === "live" ? " ⚠ LIVE" : "";
1277
+ return `${(theme.name || "(unnamed)").padEnd(40)} ID: ${theme.id.padEnd(14)} role: ${theme.role || "unknown"}${liveWarning}`;
1278
+ },
1279
+ currentIndex
1280
+ );
1281
+ let chosenTheme = selected;
1282
+ if (selected.role === "live") {
1283
+ console.log("\n╔══════════════════════════════════════════════════════════╗");
1284
+ console.log("║ ⚠ LIVE THEME SELECTED — READ BEFORE CONTINUING ║");
1285
+ console.log("╚══════════════════════════════════════════════════════════╝");
1286
+ console.log("\nConsequences:");
1287
+ console.log(" • Every `theme:push` will IMMEDIATELY update your public storefront");
1288
+ console.log(" • Broken Liquid or CSS will be visible to customers instantly");
1289
+ console.log(" • No auto-revert — you must manually republish the previous theme to roll back\n");
1290
+ const unpublished = result.themes.filter((t) => t.role !== "live");
1291
+ const liveChoice = await choose("What would you like to do?", ["switch", "live"],
1292
+ (c) => c === "switch"
1293
+ ? `Switch to unpublished theme${unpublished[0] ? ` → ${unpublished[0].name}` : ""} (RECOMMENDED)`
1294
+ : "Continue with LIVE theme (I accept the risk)"
1295
+ );
1296
+ if (liveChoice === "switch") {
1297
+ if (!unpublished.length) { console.log("No unpublished themes found. Aborting."); process.exit(0); }
1298
+ chosenTheme = unpublished.length > 1
1299
+ ? await choose("Select unpublished theme", unpublished,
1300
+ (t) => `${(t.name || "(unnamed)").padEnd(40)} ID: ${t.id}`)
1301
+ : unpublished[0];
1302
+ console.log(`Switched to: ${chosenTheme.name} (${chosenTheme.id})`);
1303
+ } else {
1304
+ console.log(`Confirmed LIVE theme: ${selected.name}. Every push goes live immediately.`);
1305
+ }
1306
+ }
1307
+ const activeName = cfg.profile || "default";
1308
+ const current = loadProfile(activeName);
1309
+ const saved = saveProfile({ ...current, name: activeName, themeId: chosenTheme.id, themeName: chosenTheme.name, themeRole: chosenTheme.role || "" });
1310
+ fs.writeFileSync(configPath, JSON.stringify({ profile: saved.name }, null, 2) + "\n");
1311
+ const envStatus = writeLocalEnv(saved, saved.themeToken || "");
1312
+ console.log(`\nActive theme: ${chosenTheme.id}${chosenTheme.name ? ` (${chosenTheme.name})` : ""}`);
1313
+ console.log(`.env ${envStatus === "created" ? "created" : "updated"} with new SHOPIFY_THEME_ID.`);
1314
+ }
1315
+
1316
+ async function themeCommand(kind) {
1317
+ const cfg = loadConfig();
1318
+ const env = themeEnv(cfg);
1319
+ const isLive = cfg.themeRole === "live";
1320
+
1321
+ if (["pull", "push", "publish"].includes(kind)) {
1322
+ ensureCleanGitWorktree(`theme:${kind}`);
1323
+ }
1324
+
1325
+ if (kind === "check") {
1326
+ const themePath = requirePulledThemeDirectory(cfg, "theme:check");
1327
+ run("shopify", ["theme", "check", "--path", themePath], { env });
1328
+ }
1329
+
1330
+ if (kind === "dev") {
1331
+ const themePath = requirePulledThemeDirectory(cfg, "theme:dev");
1332
+ if (isLive) {
1333
+ console.log("\n⚠ Active theme is LIVE. The dev server will sync changes to your public storefront.");
1334
+ console.log(" Run `shopify-agent theme:select` to switch to an unpublished theme first.\n");
1335
+ const ok = await confirm("Start dev server on LIVE theme anyway?", false);
1336
+ if (!ok) { console.log("Aborted. Switch to an unpublished theme first."); process.exit(0); }
1337
+ }
1338
+ const args = shopifyArgsWithStore(cfg, ["theme", "dev"]);
1339
+ if (cfg.themeId) args.push("--theme", cfg.themeId);
1340
+ args.push("--path", themePath);
1341
+ args.push("--allow-live", "--open");
1342
+ run("shopify", args, { env });
1343
+ }
1344
+
1345
+ if (kind === "pull") {
1346
+ const themePath = themeDirectory(cfg);
1347
+ fs.mkdirSync(themePath, { recursive: true });
1348
+ assertThemeDirectoryMatches(themePath, cfg);
1349
+ const args = shopifyArgsWithStore(cfg, ["theme", "pull"]);
1350
+ if (cfg.themeId) args.push("--theme", cfg.themeId);
1351
+ args.push("--path", themePath);
1352
+ const status = run("shopify", args, { env });
1353
+ if (status !== 0) return;
1354
+ writeThemeMetadata(themePath, cfg);
1355
+ console.log(`\nTheme files are in: ${path.relative(root, themePath)}`);
1356
+ console.log("Commit this pull before the next Shopify theme operation:");
1357
+ console.log(` git add ${path.relative(root, themePath)}`);
1358
+ console.log(` git commit -m "Pull ${cfg.themeName || cfg.themeId} theme"`);
1359
+ }
1360
+
1361
+ if (kind === "push") {
1362
+ const themePath = requirePulledThemeDirectory(cfg, "theme:push");
1363
+ if (isLive) {
1364
+ console.log("\n╔══════════════════════════════════════════════════════════════╗");
1365
+ console.log("║ ⚠ YOU ARE ABOUT TO PUSH TO YOUR LIVE THEME ║");
1366
+ console.log("╚══════════════════════════════════════════════════════════════╝");
1367
+ console.log("\n Theme : " + (cfg.themeName || cfg.themeId));
1368
+ console.log(" Store : " + cfg.store);
1369
+ console.log("\nConsequences:");
1370
+ console.log(" • Your public storefront updates IMMEDIATELY after this push");
1371
+ console.log(" • Every customer on your site right now will see the new code");
1372
+ console.log(" • Any Liquid errors, broken CSS, or JS issues go live instantly");
1373
+ console.log(" • To roll back you must manually republish the previous live theme");
1374
+ console.log("\nRecommended: push to a new staging theme → preview → then publish.\n");
1375
+
1376
+ const pushChoice = await choose("How would you like to proceed?", ["staging", "live", "cancel"],
1377
+ (c) => ({
1378
+ staging: "Push to a NEW unpublished staging theme (safe — preview before publishing) [RECOMMENDED]",
1379
+ live: "Push directly to LIVE theme (I accept full responsibility for the consequences)",
1380
+ cancel: "Cancel"
1381
+ })[c]
1382
+ );
1383
+
1384
+ if (pushChoice === "cancel") { console.log("Cancelled."); process.exit(0); }
1385
+
1386
+ if (pushChoice === "staging") {
1387
+ console.log("\nCreating new unpublished staging theme from your local files...");
1388
+ const args = shopifyArgsWithStore(cfg, ["theme", "push", "--unpublished"]);
1389
+ args.push("--path", themePath);
1390
+ run("shopify", args, { env });
1391
+ console.log("\nDone. A new unpublished theme has been created on your store.");
1392
+ console.log("Preview it in Shopify Admin → Online Store → Themes.");
1393
+ console.log("When satisfied, publish it with: shopify-agent theme:publish");
1394
+ return;
1395
+ }
1396
+
1397
+ // live push — double confirmation
1398
+ console.log("\nFinal confirmation required.");
1399
+ const typedConfirm = await ask(`Type the store domain to confirm (${cfg.store})`, "");
1400
+ if (typedConfirm.trim() !== cfg.store) {
1401
+ console.log("Store domain did not match. Push cancelled.");
1402
+ process.exit(1);
1403
+ }
1404
+ }
1405
+
1406
+ const args = shopifyArgsWithStore(cfg, ["theme", "push"]);
1407
+ if (cfg.themeId) args.push("--theme", cfg.themeId);
1408
+ args.push("--path", themePath);
1409
+ if (isLive) args.push("--allow-live");
1410
+ run("shopify", args, { env });
1411
+ }
1412
+
1413
+ if (kind === "publish") {
1414
+ if (!cfg.themeId) {
1415
+ console.error("Missing SHOPIFY_THEME_ID. Run `shopify-agent theme:select` first.");
1416
+ process.exit(1);
1417
+ }
1418
+ const themeName = cfg.themeName || cfg.themeId;
1419
+ console.log(`\nPublish theme: ${themeName}`);
1420
+ console.log(`Store: ${cfg.store}`);
1421
+ console.log("\n⚠️ This will make the theme LIVE for all visitors immediately.");
1422
+ console.log(" There is no auto-revert — to roll back you must manually republish the previous theme.\n");
1423
+ const ok = await confirm("Publish this theme to your live storefront?", false);
1424
+ if (!ok) { console.log("Publish cancelled."); process.exit(0); }
1425
+ console.log(`\nPublishing ${themeName} to ${cfg.store}...`);
1426
+ run("shopify", shopifyArgsWithStore(cfg, ["theme", "publish", "--theme", cfg.themeId]), { env });
1427
+ }
1428
+ }
1429
+
1430
+ function appCommand(kind) {
1431
+ const cfg = loadConfig();
1432
+ const args = ["app", kind];
1433
+ if (cfg.appConfig) args.push("--config", cfg.appConfig);
1434
+ if (cfg.appClientId) args.push("--client-id", cfg.appClientId);
1435
+ run("shopify", args);
1436
+ }
1437
+
1438
+ function requireStoreExecute() {
1439
+ if (!hasShopifyCommand("store execute")) {
1440
+ console.error("shopify store execute not available. Upgrade: npm install -g @shopify/cli@latest");
1441
+ process.exit(1);
1442
+ }
1443
+ }
1444
+
1445
+ function storeExecuteArgs(cfg) {
1446
+ const help = captureShopify(["store", "execute", "--help"]).output;
1447
+ return { help };
1448
+ }
1449
+
1450
+ function buildStoreExecuteArgs(cfg, queryStr, { allowMutations = false } = {}) {
1451
+ const { help } = storeExecuteArgs(cfg);
1452
+ const tmpFile = path.join(os.tmpdir(), `shopify-agent-gql-${Date.now()}.graphql`);
1453
+ fs.writeFileSync(tmpFile, queryStr, "utf8");
1454
+ const args = ["store", "execute", "--store", cfg.store];
1455
+ if (help.includes("--version")) args.push("--version", cfg.apiVersion);
1456
+ if (help.includes("--query-file")) {
1457
+ args.push("--query-file", tmpFile);
1458
+ } else {
1459
+ args.push("--query", queryStr);
1460
+ }
1461
+ if (allowMutations) args.push("--allow-mutations");
1462
+ return { args, tmpFile };
1463
+ }
1464
+
1465
+ function captureAdminQuery(queryStr, cfg) {
1466
+ requireStoreExecute();
1467
+ const { args, tmpFile } = buildStoreExecuteArgs(cfg, queryStr);
1468
+ try {
1469
+ const result = captureShopify(args);
1470
+ if (!result.ok) {
1471
+ const out = result.stderr || result.output || "";
1472
+ if (out.includes("No stored app authentication")) {
1473
+ console.error("GraphQL query failed: no Admin API token for " + cfg.store);
1474
+ console.error("\nRun this first to authenticate:\n");
1475
+ console.error(" shopify-agent store:auth\n");
1476
+ console.error("This opens a browser to approve Admin API scopes, then stores the token.\n");
1477
+ } else {
1478
+ console.error("GraphQL query failed:\n" + out);
1479
+ }
1480
+ process.exit(1);
1481
+ }
1482
+ const cleaned = stripAnsi(result.stdout);
1483
+ const jsonStart = cleaned.indexOf("{");
1484
+ if (jsonStart === -1) {
1485
+ console.error("No JSON found in response:\n" + result.output);
1486
+ process.exit(1);
1487
+ }
1488
+ try {
1489
+ const parsed = JSON.parse(cleaned.slice(jsonStart));
1490
+ return Object.prototype.hasOwnProperty.call(parsed, "data") ? parsed : { data: parsed };
1491
+ } catch (e) {
1492
+ console.error("Failed to parse GraphQL response:\n" + result.output);
1493
+ process.exit(1);
1494
+ }
1495
+ } finally {
1496
+ try { fs.unlinkSync(tmpFile); } catch {}
1497
+ }
1498
+ }
1499
+
1500
+ function runAdminMutation(mutationStr, cfg) {
1501
+ requireStoreExecute();
1502
+ const { args, tmpFile } = buildStoreExecuteArgs(cfg, mutationStr, { allowMutations: true });
1503
+ try {
1504
+ const result = captureShopify(args);
1505
+ if (!result.ok) {
1506
+ const out = result.stderr || result.output || "";
1507
+ if (out.includes("No stored app authentication")) {
1508
+ console.error("Mutation failed: no Admin API token for " + cfg.store);
1509
+ console.error("\nRun this first to authenticate:\n");
1510
+ console.error(" shopify-agent store:auth\n");
1511
+ console.error("This opens a browser to approve Admin API scopes, then stores the token.\n");
1512
+ } else {
1513
+ console.error("Mutation failed:\n" + out);
1514
+ }
1515
+ process.exit(1);
1516
+ }
1517
+ // Print any output from the mutation (user errors, result IDs, etc.)
1518
+ const cleaned = stripAnsi(result.stdout || "").trim();
1519
+ if (cleaned) console.log(cleaned);
1520
+ } finally {
1521
+ try { fs.unlinkSync(tmpFile); } catch {}
1522
+ }
1523
+ }
1524
+
1525
+ // ─── Shared Admin Helpers ──────────────────────────────────────────────────
1526
+
1527
+ function parseCliFlags() {
1528
+ const args = process.argv.slice(3).filter((a) => a !== "--yes" && a !== "-y");
1529
+ const flags = {};
1530
+ for (let i = 0; i < args.length; i++) {
1531
+ if (args[i].startsWith("--")) {
1532
+ const key = args[i].slice(2);
1533
+ const next = args[i + 1];
1534
+ flags[key] = next && !next.startsWith("-") ? (i++, next) : true;
1535
+ }
1536
+ }
1537
+ return flags;
1538
+ }
1539
+
1540
+ function extractId(gid) {
1541
+ return gid ? gid.split("/").pop() : "—";
1542
+ }
1543
+
1544
+ function printTable(rows) {
1545
+ if (!rows.length) return;
1546
+ const headers = Object.keys(rows[0]);
1547
+ const widths = headers.map((h) => Math.max(h.length, ...rows.map((r) => String(r[h] ?? "").length)));
1548
+ const sep = (l, m, r) => l + widths.map((w) => "─".repeat(w + 2)).join(m) + r;
1549
+ const row = (r) => "│" + headers.map((h, i) => ` ${String(r[h] ?? "").padEnd(widths[i])} `).join("│") + "│";
1550
+ console.log(sep("┌", "┬", "┐"));
1551
+ console.log(row(Object.fromEntries(headers.map((h) => [h, h]))));
1552
+ console.log(sep("├", "┼", "┤"));
1553
+ rows.forEach((r) => console.log(row(r)));
1554
+ console.log(sep("└", "┴", "┘"));
1555
+ }
1556
+
1557
+ function esc(str) { return String(str || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"'); }
1558
+ function trunc(str, n = 35) { return String(str || "—").slice(0, n); }
1559
+
1560
+ // ─── Products ──────────────────────────────────────────────────────────────
1561
+
1562
+ async function productsList() {
1563
+ const flags = parseCliFlags();
1564
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1565
+ const statusFilter = flags.status ? `, query: "status:${flags.status}"` : "";
1566
+ const limit = parseInt(flags.limit, 10) || 20;
1567
+ const data = captureAdminQuery(`query {
1568
+ products(first: ${limit}${statusFilter}) {
1569
+ edges { node {
1570
+ id title status productType vendor totalInventory
1571
+ priceRangeV2 { minVariantPrice { amount currencyCode } }
1572
+ } }
1573
+ }
1574
+ }`, cfg);
1575
+ const items = (data.data?.products?.edges || []).map((e) => e.node);
1576
+ if (!items.length) { console.log("No products found."); return; }
1577
+ printTable(items.map((p) => ({
1578
+ ID: extractId(p.id), Title: trunc(p.title, 40), Status: p.status,
1579
+ Type: trunc(p.productType || "—", 18), Vendor: trunc(p.vendor || "—", 15),
1580
+ Inventory: p.totalInventory,
1581
+ Price: `${p.priceRangeV2.minVariantPrice.amount} ${p.priceRangeV2.minVariantPrice.currencyCode}`
1582
+ })));
1583
+ console.log(`\n${items.length} products | --status active|draft|archived | --limit N`);
1584
+ }
1585
+
1586
+ async function productsGet() {
1587
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1588
+ const data = captureAdminQuery(`query { products(first: 50) { edges { node { id title status } } } }`, cfg);
1589
+ const products = (data.data?.products?.edges || []).map((e) => e.node);
1590
+ if (!products.length) { console.log("No products found."); return; }
1591
+ const p0 = await choose("Select product:", products, (p) => `${p.title} (${p.status})`);
1592
+ const d = captureAdminQuery(`query {
1593
+ product(id: "${p0.id}") {
1594
+ id title descriptionHtml status productType vendor tags totalInventory
1595
+ priceRangeV2 { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } }
1596
+ variants(first: 20) { edges { node { id title sku price compareAtPrice inventoryQuantity selectedOptions { name value } } } }
1597
+ images(first: 3) { edges { node { url altText } } }
1598
+ }
1599
+ }`, cfg);
1600
+ const p = d.data?.product;
1601
+ if (!p) { console.log("Not found."); return; }
1602
+ console.log(`\n${"═".repeat(60)}\n${p.title}\n${"═".repeat(60)}`);
1603
+ console.log(`ID: ${extractId(p.id)} | Status: ${p.status} | Type: ${p.productType || "—"} | Vendor: ${p.vendor || "—"}`);
1604
+ console.log(`Tags: ${(p.tags || []).join(", ") || "—"} | Total inventory: ${p.totalInventory}`);
1605
+ console.log(`Price: ${p.priceRangeV2.minVariantPrice.amount}–${p.priceRangeV2.maxVariantPrice.amount} ${p.priceRangeV2.minVariantPrice.currencyCode}`);
1606
+ console.log(`\nVariants:`);
1607
+ p.variants.edges.forEach(({ node: v }) =>
1608
+ console.log(` ${v.title.padEnd(28)} SKU: ${(v.sku || "—").padEnd(14)} Price: ${String(v.price).padEnd(10)} Qty: ${v.inventoryQuantity}`)
1609
+ );
1610
+ }
1611
+
1612
+ async function productsCreate() {
1613
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1614
+ console.log("\nNew Product Wizard\n");
1615
+ const title = await ask("Product title", ""); if (!title) { console.error("Title is required."); process.exit(1); }
1616
+ const productType = await ask("Product type (e.g. T-Shirt)", "");
1617
+ const vendor = await ask("Vendor", "");
1618
+ const price = await ask("Price (e.g. 29.99)", "0.00");
1619
+ const sku = await ask("SKU (optional)", "");
1620
+ const qty = parseInt(await ask("Initial inventory quantity", "0"), 10);
1621
+ const status = await choose("Status:", ["DRAFT", "ACTIVE"], (s) => s === "ACTIVE" ? "Active (visible in store)" : "Draft (hidden)", 0);
1622
+ const ok = await confirm(`Create "${title}" at ${price} (${status})?`, false);
1623
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1624
+ const locData = captureAdminQuery(`query { locations(first: 1) { edges { node { id } } } }`, cfg);
1625
+ const locationId = locData.data?.locations?.edges?.[0]?.node?.id;
1626
+ const invBlock = locationId ? `inventoryQuantities: { availableQuantity: ${qty || 0}, locationId: "${locationId}" }` : "";
1627
+ runAdminMutation(`mutation {
1628
+ productCreate(input: {
1629
+ title: "${esc(title)}"
1630
+ ${productType ? `productType: "${esc(productType)}"` : ""}
1631
+ ${vendor ? `vendor: "${esc(vendor)}"` : ""}
1632
+ status: ${status}
1633
+ variants: [{ price: "${price}" ${sku ? `sku: "${esc(sku)}"` : ""} ${invBlock} }]
1634
+ }) {
1635
+ product { id title status }
1636
+ userErrors { field message }
1637
+ }
1638
+ }`, cfg);
1639
+ }
1640
+
1641
+ async function productsSetStatus(status) {
1642
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1643
+ const opposite = status === "ACTIVE" ? "draft OR archived" : "active";
1644
+ const data = captureAdminQuery(`query { products(first: 50) { edges { node { id title status } } } }`, cfg);
1645
+ const products = (data.data?.products?.edges || []).map((e) => e.node).filter((p) => p.status !== status);
1646
+ if (!products.length) { console.log("No eligible products found."); return; }
1647
+ const product = await choose(`Select product to ${status === "ACTIVE" ? "publish" : status === "ARCHIVED" ? "archive" : "set to " + status}:`, products, (p) => `${p.title} (${p.status})`);
1648
+ const ok = await confirm(`Set "${product.title}" to ${status}?`, false);
1649
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1650
+ runAdminMutation(`mutation {
1651
+ productUpdate(input: { id: "${product.id}", status: ${status} }) {
1652
+ product { id title status }
1653
+ userErrors { field message }
1654
+ }
1655
+ }`, cfg);
1656
+ }
1657
+
1658
+ async function productsDelete() {
1659
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1660
+ const data = captureAdminQuery(`query { products(first: 50) { edges { node { id title status } } } }`, cfg);
1661
+ const products = (data.data?.products?.edges || []).map((e) => e.node);
1662
+ if (!products.length) { console.log("No products found."); return; }
1663
+ const product = await choose("Select product to DELETE:", products, (p) => `${p.title} (${p.status})`);
1664
+ console.log(`\n⚠️ Permanently deleting: "${product.title}". This cannot be undone.`);
1665
+ const ok = await confirm("Delete this product?", false);
1666
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1667
+ runAdminMutation(`mutation {
1668
+ productDelete(input: { id: "${product.id}" }) {
1669
+ deletedProductId
1670
+ userErrors { field message }
1671
+ }
1672
+ }`, cfg);
1673
+ }
1674
+
1675
+ // ─── Variants ──────────────────────────────────────────────────────────────
1676
+
1677
+ async function variantsList() {
1678
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1679
+ const data = captureAdminQuery(`query { products(first: 50) { edges { node { id title } } } }`, cfg);
1680
+ const products = (data.data?.products?.edges || []).map((e) => e.node);
1681
+ if (!products.length) { console.log("No products found."); return; }
1682
+ const product = await choose("Select product:", products, (p) => p.title);
1683
+ const vd = captureAdminQuery(`query { product(id: "${product.id}") {
1684
+ variants(first: 50) { edges { node { id title sku price compareAtPrice inventoryQuantity barcode } } }
1685
+ } }`, cfg);
1686
+ const variants = (vd.data?.product?.variants?.edges || []).map((e) => e.node);
1687
+ if (!variants.length) { console.log("No variants found."); return; }
1688
+ printTable(variants.map((v) => ({
1689
+ ID: extractId(v.id), Title: trunc(v.title, 25), SKU: v.sku || "—",
1690
+ Price: v.price, "Compare At": v.compareAtPrice || "—", Inventory: v.inventoryQuantity, Barcode: v.barcode || "—"
1691
+ })));
1692
+ }
1693
+
1694
+ async function variantsUpdate() {
1695
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1696
+ const pd = captureAdminQuery(`query { products(first: 50) { edges { node { id title } } } }`, cfg);
1697
+ const product = await choose("Select product:", (pd.data?.products?.edges || []).map((e) => e.node), (p) => p.title);
1698
+ const vd = captureAdminQuery(`query { product(id: "${product.id}") { variants(first: 50) { edges { node { id title sku price inventoryQuantity } } } } }`, cfg);
1699
+ const variants = (vd.data?.product?.variants?.edges || []).map((e) => e.node);
1700
+ const variant = await choose("Select variant:", variants, (v) => `${v.title} — ${v.price} — qty: ${v.inventoryQuantity}`);
1701
+ console.log(`\nUpdating: ${variant.title} (leave blank to keep current)`);
1702
+ const price = await ask("New price", variant.price);
1703
+ const sku = await ask("New SKU", variant.sku || "");
1704
+ const compareAtPrice = await ask("Compare-at price (or blank)", "");
1705
+ const barcode = await ask("Barcode (or blank)", "");
1706
+ const ok = await confirm("Apply updates?", false);
1707
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1708
+ const fields = [`id: "${variant.id}"`, `price: "${price}"`];
1709
+ if (sku) fields.push(`sku: "${esc(sku)}"`);
1710
+ if (compareAtPrice) fields.push(`compareAtPrice: "${compareAtPrice}"`);
1711
+ if (barcode) fields.push(`barcode: "${esc(barcode)}"`);
1712
+ runAdminMutation(`mutation {
1713
+ productVariantUpdate(input: { ${fields.join(", ")} }) {
1714
+ productVariant { id title price sku }
1715
+ userErrors { field message }
1716
+ }
1717
+ }`, cfg);
1718
+ }
1719
+
1720
+ // ─── Collections ───────────────────────────────────────────────────────────
1721
+
1722
+ async function collectionsList() {
1723
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1724
+ const data = captureAdminQuery(`query {
1725
+ collections(first: 30) {
1726
+ edges { node { id title productsCount sortOrder ruleSet { rules { column relation condition } } } }
1727
+ }
1728
+ }`, cfg);
1729
+ const items = (data.data?.collections?.edges || []).map((e) => e.node);
1730
+ if (!items.length) { console.log("No collections found."); return; }
1731
+ printTable(items.map((c) => ({
1732
+ ID: extractId(c.id), Title: trunc(c.title, 40), Products: c.productsCount,
1733
+ Type: c.ruleSet ? "Smart" : "Manual", Sort: c.sortOrder || "—"
1734
+ })));
1735
+ }
1736
+
1737
+ async function collectionsCreate() {
1738
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1739
+ console.log("\nNew Collection Wizard\n");
1740
+ const title = await ask("Collection title", ""); if (!title) { console.error("Title required."); process.exit(1); }
1741
+ const description = await ask("Description (optional)", "");
1742
+ const type = await choose("Type:", ["manual", "smart"], (t) => t === "smart" ? "Smart (auto rules)" : "Manual (add products yourself)", 0);
1743
+ let ruleSetInput = "";
1744
+ if (type === "smart") {
1745
+ const column = await choose("Rule column:", ["TITLE", "TYPE", "VENDOR", "TAG", "VARIANT_PRICE"], (c) => c, 0);
1746
+ const relation = await choose("Relation:", ["CONTAINS", "EQUALS", "STARTS_WITH", "GREATER_THAN", "LESS_THAN"], (r) => r.toLowerCase(), 0);
1747
+ const condition = await ask("Condition value", "");
1748
+ ruleSetInput = `ruleSet: { appliedDisjunctively: false, rules: [{ column: ${column}, relation: ${relation}, condition: "${esc(condition)}" }] }`;
1749
+ }
1750
+ const ok = await confirm(`Create collection "${title}" (${type})?`, false);
1751
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1752
+ runAdminMutation(`mutation {
1753
+ collectionCreate(input: {
1754
+ title: "${esc(title)}"
1755
+ ${description ? `descriptionHtml: "${esc(description)}"` : ""}
1756
+ ${ruleSetInput}
1757
+ }) {
1758
+ collection { id title productsCount }
1759
+ userErrors { field message }
1760
+ }
1761
+ }`, cfg);
1762
+ }
1763
+
1764
+ // ─── Inventory ─────────────────────────────────────────────────────────────
1765
+
1766
+ async function inventoryList() {
1767
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1768
+ const data = captureAdminQuery(`query {
1769
+ inventoryItems(first: 50) {
1770
+ edges { node {
1771
+ id sku tracked
1772
+ variant { displayName product { title } }
1773
+ inventoryLevels(first: 5) { edges { node {
1774
+ location { name }
1775
+ quantities(names: ["available"]) { name quantity }
1776
+ } } }
1777
+ } }
1778
+ }
1779
+ }`, cfg);
1780
+ const items = (data.data?.inventoryItems?.edges || []).map((e) => e.node);
1781
+ if (!items.length) { console.log("No inventory items found."); return; }
1782
+ printTable(items.map((item) => {
1783
+ const levels = (item.inventoryLevels?.edges || []).map((l) => `${l.node.location.name}: ${l.node.quantities?.[0]?.quantity ?? "?"}`).join(" | ");
1784
+ return {
1785
+ SKU: item.sku || "—", Product: trunc(item.variant?.product?.title || "—", 30),
1786
+ Variant: trunc(item.variant?.displayName || "—", 20), Tracked: item.tracked ? "Yes" : "No", Stock: levels || "—"
1787
+ };
1788
+ }));
1789
+ }
1790
+
1791
+ async function inventoryAdjust() {
1792
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1793
+ const data = captureAdminQuery(`query {
1794
+ products(first: 250) { edges { node { title variants(first: 100) { edges { node { title inventoryItem { id } inventoryQuantity } } } } } }
1795
+ locations(first: 10) { edges { node { id name isActive } } }
1796
+ }`, cfg);
1797
+ const locations = (data.data?.locations?.edges || []).map((e) => e.node).filter((l) => l.isActive);
1798
+ if (!locations.length) { console.error("No active locations."); process.exit(1); }
1799
+ const location = locations.length === 1 ? locations[0] : await choose("Select location:", locations, (l) => l.name);
1800
+ const allVariants = [];
1801
+ for (const { node: p } of (data.data?.products?.edges || []))
1802
+ for (const { node: v } of p.variants.edges)
1803
+ allVariants.push({ ...v, productTitle: p.title });
1804
+ const variant = await choose("Select variant:", allVariants, (v) => `${v.productTitle} / ${v.title} (current: ${v.inventoryQuantity})`);
1805
+ const deltaStr = await ask("Adjustment: +5, -3, or =10 (= sets absolute)", "");
1806
+ if (deltaStr.startsWith("=")) {
1807
+ const qty = parseInt(deltaStr.slice(1), 10);
1808
+ const ok = await confirm(`Set inventory to ${qty} at ${location.name}?`, false);
1809
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1810
+ runAdminMutation(`mutation { inventorySetOnHandQuantities(input: {
1811
+ reason: "correction", setQuantities: [{ inventoryItemId: "${variant.inventoryItem.id}", locationId: "${location.id}", quantity: ${qty} }]
1812
+ }) { userErrors { field message } inventoryAdjustmentGroup { changes { delta quantityAfterChange } } } }`, cfg);
1813
+ } else {
1814
+ const delta = parseInt(deltaStr, 10);
1815
+ if (isNaN(delta)) { console.error("Invalid. Use +5, -3, or =10."); process.exit(1); }
1816
+ const ok = await confirm(`Adjust by ${delta > 0 ? "+" : ""}${delta} at ${location.name}?`, false);
1817
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1818
+ runAdminMutation(`mutation { inventoryAdjustQuantities(input: {
1819
+ reason: "correction", name: "available",
1820
+ changes: [{ inventoryItemId: "${variant.inventoryItem.id}", locationId: "${location.id}", delta: ${delta} }]
1821
+ }) { userErrors { field message } inventoryAdjustmentGroup { changes { delta quantityAfterChange } } } }`, cfg);
1822
+ }
1823
+ }
1824
+
1825
+ // ─── Orders ────────────────────────────────────────────────────────────────
1826
+
1827
+ async function ordersList() {
1828
+ const flags = parseCliFlags();
1829
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1830
+ const status = flags.status || "open";
1831
+ const limit = parseInt(flags.limit, 10) || 20;
1832
+ const data = captureAdminQuery(`query {
1833
+ orders(first: ${limit}, query: "status:${status}", sortKey: CREATED_AT, reverse: true) {
1834
+ edges { node {
1835
+ id name createdAt displayFinancialStatus displayFulfillmentStatus
1836
+ totalPriceSet { presentmentMoney { amount currencyCode } }
1837
+ customer { displayName }
1838
+ lineItems(first: 2) { edges { node { title quantity } } }
1839
+ } }
1840
+ }
1841
+ }`, cfg);
1842
+ const orders = (data.data?.orders?.edges || []).map((e) => e.node);
1843
+ if (!orders.length) { console.log(`No ${status} orders found.`); return; }
1844
+ printTable(orders.map((o) => ({
1845
+ Order: o.name, Date: o.createdAt.slice(0, 10), Customer: trunc(o.customer?.displayName || "Guest", 22),
1846
+ Total: `${o.totalPriceSet.presentmentMoney.amount} ${o.totalPriceSet.presentmentMoney.currencyCode}`,
1847
+ Payment: o.displayFinancialStatus, Fulfillment: o.displayFulfillmentStatus
1848
+ })));
1849
+ console.log(`\n${orders.length} orders | --status open|closed|cancelled|any | --limit N`);
1850
+ }
1851
+
1852
+ async function ordersGet() {
1853
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1854
+ const data = captureAdminQuery(`query { orders(first: 30, query: "status:any", sortKey: CREATED_AT, reverse: true) {
1855
+ edges { node { id name createdAt customer { displayName } totalPriceSet { presentmentMoney { amount currencyCode } } } }
1856
+ } }`, cfg);
1857
+ const orders = (data.data?.orders?.edges || []).map((e) => e.node);
1858
+ if (!orders.length) { console.log("No orders found."); return; }
1859
+ const o0 = await choose("Select order:", orders, (o) => `${o.name} ${o.createdAt.slice(0, 10)} ${o.customer?.displayName || "Guest"} ${o.totalPriceSet.presentmentMoney.amount}`);
1860
+ const d = captureAdminQuery(`query { order(id: "${o0.id}") {
1861
+ id name createdAt displayFinancialStatus displayFulfillmentStatus
1862
+ totalPriceSet { presentmentMoney { amount currencyCode } }
1863
+ subtotalPriceSet { presentmentMoney { amount currencyCode } }
1864
+ totalShippingPriceSet { presentmentMoney { amount currencyCode } }
1865
+ totalTaxSet { presentmentMoney { amount currencyCode } }
1866
+ customer {
1867
+ displayName
1868
+ defaultEmailAddress { emailAddress }
1869
+ defaultPhoneNumber { phoneNumber }
1870
+ }
1871
+ shippingAddress { address1 city province country zip }
1872
+ lineItems(first: 20) { edges { node { title quantity originalUnitPriceSet { presentmentMoney { amount currencyCode } } variant { sku } } } }
1873
+ fulfillments { status trackingInfo { number url } }
1874
+ note tags
1875
+ } }`, cfg);
1876
+ const o = d.data?.order; if (!o) { console.log("Not found."); return; }
1877
+ console.log(`\n${"═".repeat(62)}\nOrder ${o.name} — ${o.createdAt.slice(0, 10)}\n${"═".repeat(62)}`);
1878
+ console.log(`Customer: ${o.customer?.displayName || "Guest"} <${o.customer?.defaultEmailAddress?.emailAddress || "—"}>`);
1879
+ if (o.shippingAddress) { const a = o.shippingAddress; console.log(`Ship to: ${[a.address1, a.city, a.province, a.country].filter(Boolean).join(", ")}`); }
1880
+ console.log(`Payment: ${o.displayFinancialStatus} | Fulfillment: ${o.displayFulfillmentStatus}`);
1881
+ if (o.fulfillments?.length) o.fulfillments.forEach((f) => f.trackingInfo?.forEach((t) => console.log(`Tracking: ${t.number} ${t.url}`)));
1882
+ console.log(`\nLine Items:`);
1883
+ o.lineItems.edges.forEach(({ node: li }) =>
1884
+ console.log(` ${String(li.quantity).padStart(2)}× ${li.title.padEnd(42)} ${li.originalUnitPriceSet.presentmentMoney.amount}`)
1885
+ );
1886
+ const m = o.totalPriceSet.presentmentMoney;
1887
+ console.log(`\n Subtotal: ${o.subtotalPriceSet.presentmentMoney.amount} | Shipping: ${o.totalShippingPriceSet.presentmentMoney.amount} | Tax: ${o.totalTaxSet.presentmentMoney.amount} | Total: ${m.amount} ${m.currencyCode}`);
1888
+ if (o.note) console.log(`Note: ${o.note}`);
1889
+ }
1890
+
1891
+ async function ordersCancel() {
1892
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1893
+ const data = captureAdminQuery(`query { orders(first: 30, query: "status:open") {
1894
+ edges { node { id name customer { displayName } totalPriceSet { presentmentMoney { amount currencyCode } } } }
1895
+ } }`, cfg);
1896
+ const orders = (data.data?.orders?.edges || []).map((e) => e.node);
1897
+ if (!orders.length) { console.log("No open orders to cancel."); return; }
1898
+ const order = await choose("Select order to cancel:", orders, (o) => `${o.name} ${o.customer?.displayName || "Guest"} ${o.totalPriceSet.presentmentMoney.amount}`);
1899
+ const reason = await choose("Reason:", ["CUSTOMER", "FRAUD", "INVENTORY", "DECLINED", "OTHER"], (r) => r.charAt(0) + r.slice(1).toLowerCase(), 4);
1900
+ const refund = await confirm("Refund payment?", false);
1901
+ const restock = await confirm("Restock inventory?", true);
1902
+ console.log(`\n⚠️ Cancelling order ${order.name}. This cannot be undone.`);
1903
+ const ok = await confirm("Confirm cancellation?", false);
1904
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1905
+ runAdminMutation(`mutation {
1906
+ orderCancel(orderId: "${order.id}", reason: ${reason}, refund: ${refund}, restock: ${restock}) {
1907
+ job { id }
1908
+ orderCancelUserErrors { field message }
1909
+ }
1910
+ }`, cfg);
1911
+ }
1912
+
1913
+ async function ordersClose() {
1914
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1915
+ const data = captureAdminQuery(`query { orders(first: 30, query: "status:open") {
1916
+ edges { node { id name customer { displayName } } }
1917
+ } }`, cfg);
1918
+ const orders = (data.data?.orders?.edges || []).map((e) => e.node);
1919
+ if (!orders.length) { console.log("No open orders."); return; }
1920
+ const order = await choose("Select order to close:", orders, (o) => `${o.name} ${o.customer?.displayName || "Guest"}`);
1921
+ const ok = await confirm(`Close order ${order.name}?`, false);
1922
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
1923
+ runAdminMutation(`mutation {
1924
+ orderClose(input: { id: "${order.id}" }) {
1925
+ order { id name displayFulfillmentStatus }
1926
+ userErrors { field message }
1927
+ }
1928
+ }`, cfg);
1929
+ }
1930
+
1931
+ // ─── Customers ─────────────────────────────────────────────────────────────
1932
+
1933
+ async function customersList() {
1934
+ const flags = parseCliFlags();
1935
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1936
+ const limit = parseInt(flags.limit, 10) || 20;
1937
+ const data = captureAdminQuery(`query {
1938
+ customers(first: ${limit}) {
1939
+ edges { node {
1940
+ id displayName numberOfOrders
1941
+ defaultEmailAddress { emailAddress }
1942
+ defaultPhoneNumber { phoneNumber }
1943
+ amountSpent { amount currencyCode }
1944
+ defaultAddress { city country }
1945
+ createdAt
1946
+ } }
1947
+ }
1948
+ }`, cfg);
1949
+ const items = (data.data?.customers?.edges || []).map((e) => e.node);
1950
+ if (!items.length) { console.log("No customers found."); return; }
1951
+ printTable(items.map((c) => ({
1952
+ ID: extractId(c.id), Name: trunc(c.displayName || "—", 25), Email: trunc(c.defaultEmailAddress?.emailAddress || "—", 30),
1953
+ Orders: c.numberOfOrders || 0, Spent: `${c.amountSpent?.amount || "0"} ${c.amountSpent?.currencyCode || ""}`,
1954
+ Location: c.defaultAddress ? `${c.defaultAddress.city || "—"}, ${c.defaultAddress.country || ""}` : "—"
1955
+ })));
1956
+ console.log(`\n${items.length} customers | --limit N`);
1957
+ }
1958
+
1959
+ async function customersSearch() {
1960
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1961
+ const query = await ask("Search (name, email, phone, tag)", "");
1962
+ if (!query) { console.error("Search query required."); process.exit(1); }
1963
+ const data = captureAdminQuery(`query {
1964
+ customers(first: 20, query: "${esc(query)}") {
1965
+ edges { node {
1966
+ id displayName numberOfOrders
1967
+ defaultEmailAddress { emailAddress }
1968
+ defaultPhoneNumber { phoneNumber }
1969
+ amountSpent { amount currencyCode }
1970
+ defaultAddress { city country }
1971
+ } }
1972
+ }
1973
+ }`, cfg);
1974
+ const items = (data.data?.customers?.edges || []).map((e) => e.node);
1975
+ if (!items.length) { console.log(`No customers matching "${query}".`); return; }
1976
+ printTable(items.map((c) => ({
1977
+ ID: extractId(c.id), Name: trunc(c.displayName || "—", 25), Email: trunc(c.defaultEmailAddress?.emailAddress || "—", 30),
1978
+ Phone: c.defaultPhoneNumber?.phoneNumber || "—", Orders: c.numberOfOrders, Spent: `${c.amountSpent?.amount || "0"} ${c.amountSpent?.currencyCode || ""}`
1979
+ })));
1980
+ }
1981
+
1982
+ async function customersGet() {
1983
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
1984
+ const q = await ask("Search to find customer (name or email)", "");
1985
+ const data = captureAdminQuery(`query { customers(first: 20, query: "${esc(q)}") {
1986
+ edges { node { id displayName defaultEmailAddress { emailAddress } } }
1987
+ } }`, cfg);
1988
+ const customers = (data.data?.customers?.edges || []).map((e) => e.node);
1989
+ if (!customers.length) { console.log("No customers found."); return; }
1990
+ const c0 = await choose("Select customer:", customers, (c) => `${c.displayName} <${c.defaultEmailAddress?.emailAddress || "no email"}>`);
1991
+ const d = captureAdminQuery(`query { customer(id: "${c0.id}") {
1992
+ id displayName firstName lastName numberOfOrders
1993
+ defaultEmailAddress { emailAddress }
1994
+ defaultPhoneNumber { phoneNumber }
1995
+ amountSpent { amount currencyCode }
1996
+ tags verifiedEmail taxExempt createdAt
1997
+ defaultAddress { address1 city province country zip }
1998
+ orders(first: 5, sortKey: CREATED_AT, reverse: true) {
1999
+ edges { node { name createdAt totalPriceSet { presentmentMoney { amount currencyCode } } displayFinancialStatus } }
2000
+ }
2001
+ } }`, cfg);
2002
+ const c = d.data?.customer; if (!c) { console.log("Not found."); return; }
2003
+ console.log(`\n${"═".repeat(60)}\n${c.displayName}\n${"═".repeat(60)}`);
2004
+ console.log(`Email: ${c.defaultEmailAddress?.emailAddress || "—"} ${c.verifiedEmail ? "(verified)" : "(unverified)"} | Phone: ${c.defaultPhoneNumber?.phoneNumber || "—"}`);
2005
+ console.log(`Orders: ${c.numberOfOrders} | Spent: ${c.amountSpent?.amount} ${c.amountSpent?.currencyCode} | Tax Exempt: ${c.taxExempt ? "Yes" : "No"}`);
2006
+ console.log(`Tags: ${(c.tags || []).join(", ") || "—"}`);
2007
+ if (c.defaultAddress) { const a = c.defaultAddress; console.log(`Address: ${[a.address1, a.city, a.province, a.country].filter(Boolean).join(", ")}`); }
2008
+ console.log(`\nRecent Orders:`);
2009
+ c.orders.edges.forEach(({ node: o }) => console.log(` ${o.name} ${o.createdAt.slice(0, 10)} ${o.totalPriceSet.presentmentMoney.amount} ${o.totalPriceSet.presentmentMoney.currencyCode} ${o.displayFinancialStatus}`));
2010
+ }
2011
+
2012
+ async function customersCreate() {
2013
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2014
+ console.log("\nNew Customer Wizard\n");
2015
+ const firstName = await ask("First name", "");
2016
+ const lastName = await ask("Last name", "");
2017
+ const email = await ask("Email", "");
2018
+ if (!email && !firstName) { console.error("Name or email required."); process.exit(1); }
2019
+ const phone = await ask("Phone (optional)", "");
2020
+ const tags = await ask("Tags (comma-separated, optional)", "");
2021
+ const sendEmail = await confirm("Subscribe to marketing emails?", false);
2022
+ const ok = await confirm(`Create customer ${[firstName, lastName].filter(Boolean).join(" ")} <${email}>?`, false);
2023
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2024
+ const fields = [];
2025
+ if (firstName) fields.push(`firstName: "${esc(firstName)}"`);
2026
+ if (lastName) fields.push(`lastName: "${esc(lastName)}"`);
2027
+ if (email) fields.push(`email: "${esc(email)}"`);
2028
+ if (phone) fields.push(`phone: "${esc(phone)}"`);
2029
+ if (tags) fields.push(`tags: [${tags.split(",").map((t) => `"${esc(t.trim())}"`).join(",")}]`);
2030
+ if (sendEmail) fields.push(`emailMarketingConsent: { marketingState: SUBSCRIBED, marketingOptInLevel: SINGLE_OPT_IN }`);
2031
+ runAdminMutation(`mutation {
2032
+ customerCreate(input: { ${fields.join(", ")} }) {
2033
+ customer { id displayName email }
2034
+ userErrors { field message }
2035
+ }
2036
+ }`, cfg);
2037
+ }
2038
+
2039
+ async function customersTags() {
2040
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2041
+ const q = await ask("Search to find customer", "");
2042
+ const data = captureAdminQuery(`query { customers(first: 20, query: "${esc(q)}") { edges { node { id displayName email tags } } } }`, cfg);
2043
+ const customers = (data.data?.customers?.edges || []).map((e) => e.node);
2044
+ if (!customers.length) { console.log("No customers found."); return; }
2045
+ const c = await choose("Select customer:", customers, (c) => `${c.displayName} — tags: ${(c.tags || []).join(", ") || "none"}`);
2046
+ const action = await choose("Action:", ["add", "remove"], (a) => `${a} tags`, 0);
2047
+ const tagsInput = await ask(`Tags to ${action} (comma-separated)`, "");
2048
+ if (!tagsInput) { console.log("No tags."); return; }
2049
+ const tagsList = tagsInput.split(",").map((t) => `"${esc(t.trim())}"`).join(",");
2050
+ const ok = await confirm(`${action === "add" ? "Add" : "Remove"} tags [${tagsInput}] for ${c.displayName}?`, false);
2051
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2052
+ runAdminMutation(`mutation { ${action === "add" ? "tagsAdd" : "tagsRemove"}(id: "${c.id}", tags: [${tagsList}]) {
2053
+ node { id } userErrors { field message } } }`, cfg);
2054
+ }
2055
+
2056
+ // ─── Discounts ─────────────────────────────────────────────────────────────
2057
+
2058
+ async function discountsList() {
2059
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2060
+ const data = captureAdminQuery(`query {
2061
+ codeDiscountNodes(first: 30) {
2062
+ edges { node { id codeDiscount {
2063
+ __typename
2064
+ ... on DiscountCodeBasic {
2065
+ title status appliesOncePerCustomer usageLimit asyncUsageCount startsAt endsAt
2066
+ codes(first: 3) { edges { node { code } } }
2067
+ customerGets { value {
2068
+ ... on DiscountPercentage { percentage }
2069
+ ... on DiscountAmount { amount { amount currencyCode } }
2070
+ } }
2071
+ }
2072
+ ... on DiscountCodeBxgy {
2073
+ title status appliesOncePerCustomer usageLimit asyncUsageCount startsAt endsAt
2074
+ codes(first: 3) { edges { node { code } } }
2075
+ }
2076
+ ... on DiscountCodeFreeShipping {
2077
+ title status appliesOncePerCustomer usageLimit asyncUsageCount startsAt endsAt
2078
+ codes(first: 3) { edges { node { code } } }
2079
+ }
2080
+ ... on DiscountCodeApp {
2081
+ title status appliesOncePerCustomer usageLimit asyncUsageCount startsAt endsAt
2082
+ codes(first: 3) { edges { node { code } } }
2083
+ }
2084
+ } } }
2085
+ }
2086
+ }`, cfg);
2087
+ const items = (data.data?.codeDiscountNodes?.edges || []).map((e) => e.node);
2088
+ if (!items.length) { console.log("No discount codes found."); return; }
2089
+ printTable(items.map((d) => {
2090
+ const disc = d.codeDiscount;
2091
+ const codes = (disc.codes?.edges || []).map((e) => e.node.code).join(", ");
2092
+ const used = disc.asyncUsageCount || 0;
2093
+ const val = disc.customerGets?.value?.percentage
2094
+ ? `${(disc.customerGets.value.percentage * 100).toFixed(0)}% off`
2095
+ : disc.customerGets?.value?.amount
2096
+ ? `${disc.customerGets.value.amount.amount} ${disc.customerGets.value.amount.currencyCode} off`
2097
+ : (disc.__typename || "—").replace(/^DiscountCode/, "");
2098
+ return {
2099
+ Title: trunc(disc.title || "—", 28), Code: trunc(codes, 20), Value: val,
2100
+ Used: used, Limit: disc.usageLimit || "∞", "1×/customer": disc.appliesOncePerCustomer ? "Yes" : "No",
2101
+ Status: disc.status, Expires: disc.endsAt ? disc.endsAt.slice(0, 10) : "Never"
2102
+ };
2103
+ }));
2104
+ }
2105
+
2106
+ async function discountsDelete() {
2107
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2108
+ const data = captureAdminQuery(`query { codeDiscountNodes(first: 30) {
2109
+ edges { node { id codeDiscount {
2110
+ ... on DiscountCodeBasic { title status codes(first: 1) { edges { node { code } } } }
2111
+ ... on DiscountCodeBxgy { title status codes(first: 1) { edges { node { code } } } }
2112
+ ... on DiscountCodeFreeShipping { title status codes(first: 1) { edges { node { code } } } }
2113
+ ... on DiscountCodeApp { title status codes(first: 1) { edges { node { code } } } }
2114
+ } } }
2115
+ } }`, cfg);
2116
+ const items = (data.data?.codeDiscountNodes?.edges || []).map((e) => e.node);
2117
+ if (!items.length) { console.log("No discounts found."); return; }
2118
+ const d = await choose("Select discount to delete:", items, (d) => {
2119
+ const code = d.codeDiscount.codes?.edges?.[0]?.node?.code || "—";
2120
+ return `${d.codeDiscount.title} (${code}) — ${d.codeDiscount.status}`;
2121
+ });
2122
+ console.log(`\n⚠️ Permanently deleting: ${d.codeDiscount.title}`);
2123
+ const ok = await confirm("Delete this discount?", false);
2124
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2125
+ runAdminMutation(`mutation {
2126
+ discountCodeDelete(id: "${d.id}") { deletedCodeDiscountId userErrors { field message } }
2127
+ }`, cfg);
2128
+ }
2129
+
2130
+ async function discountsDisable() {
2131
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2132
+ const data = captureAdminQuery(`query { codeDiscountNodes(first: 30) {
2133
+ edges { node { id codeDiscount {
2134
+ ... on DiscountCodeBasic { title status codes(first: 1) { edges { node { code } } } }
2135
+ ... on DiscountCodeBxgy { title status codes(first: 1) { edges { node { code } } } }
2136
+ ... on DiscountCodeFreeShipping { title status codes(first: 1) { edges { node { code } } } }
2137
+ ... on DiscountCodeApp { title status codes(first: 1) { edges { node { code } } } }
2138
+ } } }
2139
+ } }`, cfg);
2140
+ const items = (data.data?.codeDiscountNodes?.edges || []).map((e) => e.node).filter((d) => d.codeDiscount.status === "ACTIVE");
2141
+ if (!items.length) { console.log("No active discounts."); return; }
2142
+ const d = await choose("Select discount to disable:", items, (d) => `${d.codeDiscount.title} (${d.codeDiscount.codes?.edges?.[0]?.node?.code || "—"})`);
2143
+ const ok = await confirm(`Disable "${d.codeDiscount.title}"?`, false);
2144
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2145
+ runAdminMutation(`mutation { discountCodeDeactivate(id: "${d.id}") { codeDiscountNode { id } userErrors { field message } } }`, cfg);
2146
+ }
2147
+
2148
+ async function discountsEnable() {
2149
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2150
+ const data = captureAdminQuery(`query { codeDiscountNodes(first: 30) {
2151
+ edges { node { id codeDiscount {
2152
+ ... on DiscountCodeBasic { title status codes(first: 1) { edges { node { code } } } }
2153
+ ... on DiscountCodeBxgy { title status codes(first: 1) { edges { node { code } } } }
2154
+ ... on DiscountCodeFreeShipping { title status codes(first: 1) { edges { node { code } } } }
2155
+ ... on DiscountCodeApp { title status codes(first: 1) { edges { node { code } } } }
2156
+ } } }
2157
+ } }`, cfg);
2158
+ const items = (data.data?.codeDiscountNodes?.edges || []).map((e) => e.node).filter((d) => d.codeDiscount.status !== "ACTIVE");
2159
+ if (!items.length) { console.log("No inactive discounts."); return; }
2160
+ const d = await choose("Select discount to enable:", items, (d) => `${d.codeDiscount.title} (${d.codeDiscount.status})`);
2161
+ const ok = await confirm(`Enable "${d.codeDiscount.title}"?`, false);
2162
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2163
+ runAdminMutation(`mutation { discountCodeActivate(id: "${d.id}") { codeDiscountNode { id } userErrors { field message } } }`, cfg);
2164
+ }
2165
+
2166
+ // ─── Gift Cards ─────────────────────────────────────────────────────────────
2167
+
2168
+ async function giftCardsList() {
2169
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2170
+ const data = captureAdminQuery(`query {
2171
+ giftCards(first: 20) {
2172
+ edges { node {
2173
+ id lastCharacters maskedCode enabled expiresOn createdAt
2174
+ initialValue { amount currencyCode } balance { amount currencyCode }
2175
+ customer { displayName email }
2176
+ } }
2177
+ }
2178
+ }`, cfg);
2179
+ const items = (data.data?.giftCards?.edges || []).map((e) => e.node);
2180
+ if (!items.length) { console.log("No gift cards found."); return; }
2181
+ printTable(items.map((c) => ({
2182
+ Code: `...${c.lastCharacters}`, Customer: trunc(c.customer?.displayName || "—", 20),
2183
+ Initial: `${c.initialValue.amount} ${c.initialValue.currencyCode}`,
2184
+ Balance: `${c.balance.amount} ${c.balance.currencyCode}`,
2185
+ Expires: c.expiresOn || "Never", Status: c.enabled ? "Active" : "Disabled"
2186
+ })));
2187
+ }
2188
+
2189
+ async function giftCardsCreate() {
2190
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2191
+ console.log("\nNew Gift Card Wizard\n");
2192
+ const amount = await ask("Amount (e.g. 500.00)", "500.00");
2193
+ const note = await ask("Note (optional)", "");
2194
+ const expiresOn = await ask("Expiry date YYYY-MM-DD (blank = no expiry)", "");
2195
+ const customerEmail = await ask("Assign to customer email (optional)", "");
2196
+ const ok = await confirm(`Create gift card worth ${amount}?`, false);
2197
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2198
+ const fields = [`initialValue: ${parseFloat(amount)}`];
2199
+ if (note) fields.push(`note: "${esc(note)}"`);
2200
+ if (expiresOn) fields.push(`expiresOn: "${expiresOn}"`);
2201
+ if (customerEmail) {
2202
+ const cData = captureAdminQuery(`query { customers(first: 1, query: "email:${esc(customerEmail)}") { edges { node { id } } } }`, cfg);
2203
+ const cId = cData.data?.customers?.edges?.[0]?.node?.id;
2204
+ if (cId) fields.push(`customerId: "${cId}"`); else console.log("Customer not found — creating unassigned gift card.");
2205
+ }
2206
+ runAdminMutation(`mutation {
2207
+ giftCardCreate(input: { ${fields.join(", ")} }) {
2208
+ giftCard { id lastCharacters initialValue { amount currencyCode } balance { amount currencyCode } }
2209
+ giftCardCode
2210
+ userErrors { field message }
2211
+ }
2212
+ }`, cfg);
2213
+ }
2214
+
2215
+ // ─── Store Info & Dashboard ────────────────────────────────────────────────
2216
+
2217
+ async function storeInfo() {
2218
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2219
+ const data = captureAdminQuery(`query { shop {
2220
+ name myshopifyDomain primaryDomain { url }
2221
+ plan { displayName } currencyCode weightUnit ianaTimezone
2222
+ email shopAddress { address1 city country phone }
2223
+ enabledPresentmentCurrencies
2224
+ checkoutApiSupported
2225
+ } }`, cfg);
2226
+ const s = data.data?.shop; if (!s) { console.log("Could not get store info."); return; }
2227
+ console.log(`\n${"═".repeat(50)}\n${s.name}\n${"═".repeat(50)}`);
2228
+ console.log(`Domain: ${s.primaryDomain?.url || s.myshopifyDomain}`);
2229
+ console.log(`Plan: ${s.plan?.displayName || "—"}`);
2230
+ console.log(`Currency: ${s.currencyCode} | Timezone: ${s.ianaTimezone} | Weight: ${s.weightUnit}`);
2231
+ console.log(`Email: ${s.email || "—"} | Phone: ${s.shopAddress?.phone || "—"}`);
2232
+ if (s.shopAddress) console.log(`Location: ${[s.shopAddress.address1, s.shopAddress.city, s.shopAddress.country].filter(Boolean).join(", ")}`);
2233
+ if (s.enabledPresentmentCurrencies?.length > 1) console.log(`Multi-currency: ${s.enabledPresentmentCurrencies.join(", ")}`);
2234
+ }
2235
+
2236
+ async function storeLocations() {
2237
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2238
+ const data = captureAdminQuery(`query { locations(first: 20) { edges { node {
2239
+ id name isActive fulfillsOnlineOrders
2240
+ address { address1 city province country zip phone }
2241
+ } } } }`, cfg);
2242
+ const items = (data.data?.locations?.edges || []).map((e) => e.node);
2243
+ if (!items.length) { console.log("No locations found."); return; }
2244
+ printTable(items.map((l) => ({
2245
+ ID: extractId(l.id), Name: l.name, Active: l.isActive ? "Yes" : "No",
2246
+ Fulfills: l.fulfillsOnlineOrders ? "Yes" : "No",
2247
+ City: l.address?.city || "—", Country: l.address?.country || "—"
2248
+ })));
2249
+ }
2250
+
2251
+ async function storeDashboard() {
2252
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2253
+ console.log(`\nFetching dashboard for ${cfg.store}...\n`);
2254
+ const data = captureAdminQuery(`query {
2255
+ shop { name currencyCode }
2256
+ orders(first: 5, query: "status:open", sortKey: CREATED_AT, reverse: true) {
2257
+ edges { node {
2258
+ name createdAt displayFinancialStatus displayFulfillmentStatus
2259
+ totalPriceSet { presentmentMoney { amount currencyCode } }
2260
+ customer { displayName }
2261
+ } }
2262
+ }
2263
+ products(first: 5, query: "status:active") {
2264
+ edges { node { id title totalInventory } }
2265
+ }
2266
+ codeDiscountNodes(first: 5) {
2267
+ edges { node { codeDiscount {
2268
+ __typename
2269
+ ... on DiscountCodeBasic {
2270
+ title status asyncUsageCount codes(first: 1) { edges { node { code } } }
2271
+ }
2272
+ ... on DiscountCodeBxgy {
2273
+ title status asyncUsageCount codes(first: 1) { edges { node { code } } }
2274
+ }
2275
+ ... on DiscountCodeFreeShipping {
2276
+ title status asyncUsageCount codes(first: 1) { edges { node { code } } }
2277
+ }
2278
+ ... on DiscountCodeApp {
2279
+ title status asyncUsageCount codes(first: 1) { edges { node { code } } }
2280
+ }
2281
+ } } }
2282
+ }
2283
+ customers(first: 1) { edges { node { id } } }
2284
+ }`, cfg);
2285
+ const shop = data.data?.shop;
2286
+ const orders = (data.data?.orders?.edges || []).map((e) => e.node);
2287
+ const products = (data.data?.products?.edges || []).map((e) => e.node);
2288
+ const discounts = (data.data?.codeDiscountNodes?.edges || []).map((e) => e.node);
2289
+ console.log(`${"═".repeat(60)}\n${shop?.name || cfg.store} — Dashboard\n${"═".repeat(60)}`);
2290
+ console.log(`\n📦 OPEN ORDERS (${orders.length} shown)`);
2291
+ if (orders.length) {
2292
+ orders.forEach((o) => console.log(` ${o.name.padEnd(8)} ${o.createdAt.slice(0, 10)} ${trunc(o.customer?.displayName || "Guest", 22)} ${o.totalPriceSet.presentmentMoney.amount} ${o.totalPriceSet.presentmentMoney.currencyCode} ${o.displayFulfillmentStatus}`));
2293
+ } else console.log(" No open orders.");
2294
+ console.log(`\n🛍️ ACTIVE PRODUCTS (sample)`);
2295
+ if (products.length) {
2296
+ products.forEach((p) => {
2297
+ const warn = p.totalInventory < 5 ? " ⚠️ low stock" : "";
2298
+ console.log(` ${trunc(p.title, 48)} Qty: ${p.totalInventory}${warn}`);
2299
+ });
2300
+ } else console.log(" No active products.");
2301
+ console.log(`\n🏷️ DISCOUNT CODES`);
2302
+ if (discounts.length) {
2303
+ discounts.forEach((d) => {
2304
+ const disc = d.codeDiscount;
2305
+ const code = disc.codes?.edges?.[0]?.node?.code || "—";
2306
+ const used = disc.asyncUsageCount || 0;
2307
+ console.log(` ${trunc(disc.title, 35)} Code: ${code.padEnd(14)} Used: ${used} (${disc.status})`);
2308
+ });
2309
+ } else console.log(" No discount codes.");
2310
+ console.log(`\nFor details: orders:list products:list customers:list discounts:list`);
2311
+ }
2312
+
2313
+ // ─── Content — Pages, Blogs, Articles, Redirects ──────────────────────────
2314
+
2315
+ async function pagesList() {
2316
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2317
+ const data = captureAdminQuery(`query { pages(first: 30) {
2318
+ edges { node { id title handle isPublished updatedAt } }
2319
+ } }`, cfg);
2320
+ const items = (data.data?.pages?.edges || []).map((e) => e.node);
2321
+ if (!items.length) { console.log("No pages found."); return; }
2322
+ printTable(items.map((p) => ({
2323
+ ID: extractId(p.id), Title: trunc(p.title, 40), Handle: p.handle,
2324
+ Published: p.isPublished ? "Yes" : "No", Updated: p.updatedAt.slice(0, 10)
2325
+ })));
2326
+ }
2327
+
2328
+ async function pagesCreate() {
2329
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2330
+ console.log("\nNew Page Wizard\n");
2331
+ const title = await ask("Page title", ""); if (!title) { console.error("Title required."); process.exit(1); }
2332
+ const body = await ask("Body HTML (single-line, optional)", "");
2333
+ const published = await confirm("Publish immediately?", false);
2334
+ const ok = await confirm(`Create page "${title}"?`, false);
2335
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2336
+ runAdminMutation(`mutation {
2337
+ pageCreate(page: {
2338
+ title: "${esc(title)}" ${body ? `body: "${esc(body)}"` : ""} isPublished: ${published}
2339
+ }) {
2340
+ page { id title handle isPublished }
2341
+ userErrors { field message }
2342
+ }
2343
+ }`, cfg);
2344
+ }
2345
+
2346
+ async function blogsList() {
2347
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2348
+ const data = captureAdminQuery(`query { blogs(first: 10) { edges { node {
2349
+ id title handle
2350
+ articles(first: 5, reverse: true) { edges { node { title publishedAt } } }
2351
+ } } } }`, cfg);
2352
+ const blogs = (data.data?.blogs?.edges || []).map((e) => e.node);
2353
+ if (!blogs.length) { console.log("No blogs found."); return; }
2354
+ blogs.forEach((b) => {
2355
+ console.log(`\n📝 ${b.title} (/${b.handle})`);
2356
+ const articles = b.articles.edges;
2357
+ if (articles.length) articles.forEach(({ node: a }) => console.log(` • ${trunc(a.title, 50)} ${a.publishedAt?.slice(0, 10) || "draft"}`));
2358
+ else console.log(" (no articles)");
2359
+ });
2360
+ }
2361
+
2362
+ async function articlesList() {
2363
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2364
+ const bData = captureAdminQuery(`query { blogs(first: 10) { edges { node { id title } } } }`, cfg);
2365
+ const blogs = (bData.data?.blogs?.edges || []).map((e) => e.node);
2366
+ if (!blogs.length) { console.log("No blogs found."); return; }
2367
+ const blog = await choose("Select blog:", blogs, (b) => b.title);
2368
+ const data = captureAdminQuery(`query { blog(id: "${blog.id}") {
2369
+ articles(first: 30, reverse: true) {
2370
+ edges { node { id title handle author { name } publishedAt isPublished tags } }
2371
+ }
2372
+ } }`, cfg);
2373
+ const items = (data.data?.blog?.articles?.edges || []).map((e) => e.node);
2374
+ if (!items.length) { console.log("No articles found."); return; }
2375
+ printTable(items.map((a) => ({
2376
+ ID: extractId(a.id), Title: trunc(a.title, 40), Author: trunc(a.author?.name || "—", 18),
2377
+ Published: a.isPublished ? (a.publishedAt?.slice(0, 10) || "Yes") : "Draft",
2378
+ Tags: trunc((a.tags || []).join(", "), 20)
2379
+ })));
2380
+ }
2381
+
2382
+ async function redirectsList() {
2383
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2384
+ const data = captureAdminQuery(`query { urlRedirects(first: 50) {
2385
+ edges { node { id path target } }
2386
+ } }`, cfg);
2387
+ const items = (data.data?.urlRedirects?.edges || []).map((e) => e.node);
2388
+ if (!items.length) { console.log("No redirects found."); return; }
2389
+ printTable(items.map((r) => ({ ID: extractId(r.id), From: r.path, To: trunc(r.target, 60) })));
2390
+ }
2391
+
2392
+ async function redirectsCreate() {
2393
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2394
+ const from = await ask("Redirect from (e.g. /old-page)", "");
2395
+ const to = await ask("Redirect to (e.g. /new-page)", "");
2396
+ if (!from || !to) { console.error("Both from and to required."); process.exit(1); }
2397
+ const ok = await confirm(`Create redirect: ${from} → ${to}?`, false);
2398
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2399
+ runAdminMutation(`mutation {
2400
+ urlRedirectCreate(urlRedirect: { path: "${esc(from)}", target: "${esc(to)}" }) {
2401
+ urlRedirect { id path target }
2402
+ userErrors { field message }
2403
+ }
2404
+ }`, cfg);
2405
+ }
2406
+
2407
+ // ─── Metafields ────────────────────────────────────────────────────────────
2408
+
2409
+ async function metafieldsList() {
2410
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2411
+ const ownerType = await choose("Resource type:", ["PRODUCT", "COLLECTION", "CUSTOMER", "ORDER", "SHOP"], (t) => t.charAt(0) + t.slice(1).toLowerCase(), 0);
2412
+ let ownerId = null;
2413
+ if (ownerType !== "SHOP") {
2414
+ const resourceType = ownerType.toLowerCase() + "s";
2415
+ const rData = captureAdminQuery(`query { ${resourceType}(first: 30${ownerType === "ORDER" ? ', query: "status:any"' : ""}) {
2416
+ edges { node { id ${ownerType === "CUSTOMER" ? "displayName email" : ownerType === "ORDER" ? "name" : "title"} } }
2417
+ } }`, cfg);
2418
+ const resources = (rData.data?.[resourceType]?.edges || []).map((e) => e.node);
2419
+ if (!resources.length) { console.log("No resources found."); return; }
2420
+ const res = await choose(`Select ${ownerType.toLowerCase()}:`, resources, (r) => r.title || r.name || r.displayName);
2421
+ ownerId = res.id;
2422
+ }
2423
+ const typeFragment = ownerType === "SHOP" ? "shop" : `node(id: "${ownerId}") { ... on ${ownerType.charAt(0) + ownerType.slice(1).toLowerCase()}`;
2424
+ const queryStr = ownerType === "SHOP"
2425
+ ? `query { shop { metafields(first: 30) { edges { node { id namespace key value type } } } } }`
2426
+ : `query { node(id: "${ownerId}") { ... on ${ownerType.charAt(0) + ownerType.slice(1).toLowerCase()} { metafields(first: 30) { edges { node { id namespace key value type } } } } } }`;
2427
+ const data = captureAdminQuery(queryStr, cfg);
2428
+ const mf = ownerType === "SHOP"
2429
+ ? (data.data?.shop?.metafields?.edges || []).map((e) => e.node)
2430
+ : (data.data?.node?.metafields?.edges || []).map((e) => e.node);
2431
+ if (!mf.length) { console.log("No metafields found."); return; }
2432
+ printTable(mf.map((m) => ({ ID: extractId(m.id), Namespace: m.namespace, Key: m.key, Type: m.type, Value: trunc(m.value || "", 50) })));
2433
+ }
2434
+
2435
+ async function metafieldsSet() {
2436
+ const cfg = loadConfig(); requireStore(cfg); requireStoreExecute();
2437
+ const ownerType = await choose("Resource type:", ["PRODUCT", "COLLECTION", "CUSTOMER", "ORDER", "SHOP"], (t) => t.charAt(0) + t.slice(1).toLowerCase(), 0);
2438
+ let ownerId;
2439
+ if (ownerType !== "SHOP") {
2440
+ const resourceType = ownerType.toLowerCase() + "s";
2441
+ const rData = captureAdminQuery(`query { ${resourceType}(first: 30${ownerType === "ORDER" ? ', query: "status:any"' : ""}) {
2442
+ edges { node { id ${ownerType === "CUSTOMER" ? "displayName" : ownerType === "ORDER" ? "name" : "title"} } }
2443
+ } }`, cfg);
2444
+ const resources = (rData.data?.[resourceType]?.edges || []).map((e) => e.node);
2445
+ const res = await choose(`Select ${ownerType.toLowerCase()}:`, resources, (r) => r.title || r.name || r.displayName);
2446
+ ownerId = res.id;
2447
+ } else {
2448
+ const sData = captureAdminQuery(`query { shop { id } }`, cfg);
2449
+ ownerId = sData.data?.shop?.id;
2450
+ }
2451
+ const namespace = await ask("Namespace (e.g. custom)", "custom");
2452
+ const key = await ask("Key (e.g. launch_date)", "");
2453
+ const type = await choose("Type:", ["single_line_text_field", "number_integer", "number_decimal", "boolean", "date", "json", "url"], (t) => t, 0);
2454
+ const value = await ask("Value", "");
2455
+ const ok = await confirm(`Set metafield ${namespace}.${key} = "${value}" on this ${ownerType.toLowerCase()}?`, false);
2456
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2457
+ runAdminMutation(`mutation {
2458
+ metafieldsSet(metafields: [{
2459
+ ownerId: "${ownerId}", namespace: "${esc(namespace)}", key: "${esc(key)}", type: "${type}", value: "${esc(value)}"
2460
+ }]) {
2461
+ metafields { id namespace key value type }
2462
+ userErrors { field message }
2463
+ }
2464
+ }`, cfg);
2465
+ }
2466
+
2467
+ async function inventorySet(quantityArg) {
2468
+ const qty = parseInt(quantityArg, 10);
2469
+ if (isNaN(qty) || qty < 0) {
2470
+ console.error("Usage: shopify-agent inventory:set <quantity> (e.g., inventory:set 10)");
2471
+ process.exit(1);
2472
+ }
2473
+
2474
+ const cfg = loadConfig();
2475
+ requireStore(cfg);
2476
+ requireStoreExecute();
2477
+
2478
+ console.log("Fetching products, variants, and locations...");
2479
+
2480
+ const data = captureAdminQuery(`
2481
+ query InventoryAudit {
2482
+ products(first: 250) {
2483
+ edges {
2484
+ node {
2485
+ title
2486
+ variants(first: 100) {
2487
+ edges {
2488
+ node {
2489
+ title
2490
+ inventoryItem { id }
2491
+ inventoryQuantity
2492
+ }
2493
+ }
2494
+ }
2495
+ }
2496
+ }
2497
+ }
2498
+ locations(first: 10) {
2499
+ edges {
2500
+ node { id name isActive }
2501
+ }
2502
+ }
2503
+ }`, cfg);
2504
+
2505
+ const locations = (data.data?.locations?.edges || []).map((e) => e.node).filter((l) => l.isActive);
2506
+ if (!locations.length) {
2507
+ console.error("No active locations found in your store.");
2508
+ process.exit(1);
2509
+ }
2510
+ const location = locations[0];
2511
+
2512
+ const setQuantities = [];
2513
+ for (const { node: product } of (data.data?.products?.edges || [])) {
2514
+ for (const { node: variant } of product.variants.edges) {
2515
+ setQuantities.push({
2516
+ inventoryItemId: variant.inventoryItem.id,
2517
+ locationId: location.id,
2518
+ quantity: qty,
2519
+ label: `${product.title}${variant.title !== "Default Title" ? ` / ${variant.title}` : ""}`,
2520
+ current: variant.inventoryQuantity
2521
+ });
2522
+ }
2523
+ }
2524
+
2525
+ if (!setQuantities.length) {
2526
+ console.error("No inventory items found.");
2527
+ process.exit(1);
2528
+ }
2529
+
2530
+ console.log(`\nLocation: ${location.name}`);
2531
+ console.log(`Variants to update: ${setQuantities.length}`);
2532
+ console.log(`Setting all to: ${qty}`);
2533
+ console.log("\nPreview (first 10):");
2534
+ setQuantities.slice(0, 10).forEach((item) => {
2535
+ console.log(` ${item.label}: ${item.current ?? "?"} → ${qty}`);
2536
+ });
2537
+ if (setQuantities.length > 10) console.log(` ... and ${setQuantities.length - 10} more`);
2538
+
2539
+ const ok = await confirm("\nProceed with inventory update?", false);
2540
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2541
+
2542
+ const setQtyLines = setQuantities
2543
+ .map((i) => ` { inventoryItemId: "${i.inventoryItemId}", locationId: "${i.locationId}", quantity: ${qty} }`)
2544
+ .join(",\n");
2545
+
2546
+ const mutation = `mutation SetInventoryOnHand {
2547
+ inventorySetOnHandQuantities(input: {
2548
+ reason: "correction",
2549
+ setQuantities: [
2550
+ ${setQtyLines}
2551
+ ]
2552
+ }) {
2553
+ userErrors { field message }
2554
+ inventoryAdjustmentGroup {
2555
+ reason
2556
+ changes { name delta quantityAfterChange }
2557
+ }
2558
+ }
2559
+ }`;
2560
+
2561
+ console.log("\nRunning inventory mutation...");
2562
+ runAdminMutation(mutation, cfg);
2563
+ }
2564
+
2565
+ async function discountCreate() {
2566
+
2567
+ const cfg = loadConfig();
2568
+ requireStore(cfg);
2569
+ requireStoreExecute();
2570
+
2571
+ console.log("\nDiscount Code Wizard\n");
2572
+
2573
+ const title = await ask("Discount title (internal name)", "10% Off First Order");
2574
+ const code = await ask("Discount code (customers enter this)", "WELCOME10");
2575
+
2576
+ const discountType = await choose(
2577
+ "Discount type:",
2578
+ ["percentage", "fixed_amount"],
2579
+ (t) => t === "percentage" ? "Percentage off" : "Fixed amount off",
2580
+ 0
2581
+ );
2582
+
2583
+ let valueInput;
2584
+ if (discountType === "percentage") {
2585
+ const pct = parseFloat(await ask("Percentage off (e.g. 10 for 10%)", "10"));
2586
+ valueInput = `percentage: ${pct / 100}`;
2587
+ } else {
2588
+ const amount = parseFloat(await ask("Fixed amount off (e.g. 5.00)", "5.00"));
2589
+ const currency = await ask("Currency code", "INR");
2590
+ valueInput = `discountAmount: { amount: "${amount}", appliesOnEachItem: false }`;
2591
+ }
2592
+
2593
+ const oncePerCustomer = await confirm("Limit to one use per customer? (recommended for welcome codes)", true);
2594
+ const usageLimitRaw = await ask("Total usage limit (leave blank for unlimited)", "");
2595
+ const usageLimit = usageLimitRaw ? `usageLimit: ${parseInt(usageLimitRaw, 10)}` : "";
2596
+
2597
+ const today = new Date().toISOString().split("T")[0];
2598
+ const startDate = await ask("Start date (YYYY-MM-DD)", today);
2599
+ const endDate = await ask("End date (YYYY-MM-DD, leave blank for no expiry)", "");
2600
+
2601
+ console.log("\nDiscount summary:");
2602
+ console.log(` Title: ${title}`);
2603
+ console.log(` Code: ${code}`);
2604
+ console.log(` Value: ${discountType === "percentage" ? valueInput.replace("percentage: ", "") * 100 + "%" : valueInput}`);
2605
+ console.log(` Once per customer: ${oncePerCustomer}`);
2606
+ console.log(` Usage limit: ${usageLimitRaw || "unlimited"}`);
2607
+ console.log(` Active: ${startDate}${endDate ? " → " + endDate : " (no expiry)"}`);
2608
+
2609
+ const ok = await confirm("\nCreate this discount?", false);
2610
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2611
+
2612
+ const endsAtLine = endDate ? `\n endsAt: "${endDate}T23:59:59Z"` : "";
2613
+ const usageLimitLine = usageLimit ? `\n ${usageLimit}` : "";
2614
+
2615
+ const mutation = `mutation CreateDiscountCode {
2616
+ discountCodeBasicCreate(basicCodeDiscount: {
2617
+ title: "${title}"
2618
+ code: "${code}"
2619
+ startsAt: "${startDate}T00:00:00Z"${endsAtLine}
2620
+ customerGets: {
2621
+ value: { ${valueInput} }
2622
+ items: { all: true }
2623
+ }
2624
+ customerSelection: { all: true }
2625
+ appliesOncePerCustomer: ${oncePerCustomer}${usageLimitLine}
2626
+ }) {
2627
+ codeDiscountNode {
2628
+ id
2629
+ codeDiscount {
2630
+ ... on DiscountCodeBasic {
2631
+ title
2632
+ codes(first: 1) { edges { node { code } } }
2633
+ startsAt
2634
+ appliesOncePerCustomer
2635
+ }
2636
+ }
2637
+ }
2638
+ userErrors { field message }
2639
+ }
2640
+ }`;
2641
+
2642
+ console.log("\nCreating discount...");
2643
+ runAdminMutation(mutation, cfg);
2644
+ }
2645
+
2646
+ async function adminOperation(fileArg, options = {}) {
2647
+ const cfg = loadConfig();
2648
+ requireStore(cfg);
2649
+ const file = fileArg ? path.resolve(root, fileArg) : "";
2650
+ if (!file || !fs.existsSync(file)) {
2651
+ console.error("Provide a GraphQL file path, for example templates/graphql/shop-query.graphql");
2652
+ process.exit(1);
2653
+ }
2654
+ const query = fs.readFileSync(file, "utf8");
2655
+ const kind = operationKind(query);
2656
+ const isMutation = kind === "mutation";
2657
+
2658
+ if (isMutation && !options.allowMutation) {
2659
+ console.error("Refusing to run mutation. Use `shopify-agent admin:mutate <file>` to run mutations.");
2660
+ process.exit(1);
2661
+ }
2662
+
2663
+ if (isMutation) {
2664
+ console.log(`\nMutation: ${path.basename(file)}`);
2665
+ console.log(`Store: ${cfg.store}`);
2666
+ const ok = await confirm("Run this mutation against your live store?", false);
2667
+ if (!ok) { console.log("Cancelled."); process.exit(0); }
2668
+ }
2669
+
2670
+ const useStoreExecute = hasShopifyCommand("store execute");
2671
+ const command = useStoreExecute ? ["store", "execute"] : ["app", "execute"];
2672
+ const help = captureShopify([...command, "--help"]).output;
2673
+ const args = [...command, "--store", cfg.store];
2674
+ if (help.includes("--version")) args.push("--version", cfg.apiVersion);
2675
+ if (help.includes("--query-file")) {
2676
+ args.push("--query-file", file);
2677
+ } else {
2678
+ args.push("--query", query);
2679
+ }
2680
+ if (isMutation && useStoreExecute && help.includes("--allow-mutations")) args.push("--allow-mutations");
2681
+ if (!useStoreExecute && cfg.appConfig) args.push("--config", cfg.appConfig);
2682
+ if (!useStoreExecute && cfg.appClientId) args.push("--client-id", cfg.appClientId);
2683
+ runShopify(args);
2684
+ }
2685
+
2686
+ async function adminQuery(fileArg) {
2687
+ return adminOperation(fileArg, { allowMutation: false });
2688
+ }
2689
+
2690
+ async function adminMutate(fileArg) {
2691
+ return adminOperation(fileArg, { allowMutation: true });
2692
+ }
2693
+
2694
+ async function main() {
2695
+ const positionals = process.argv.slice(2).filter((a) => !a.startsWith("-"));
2696
+ const [command, fileArg] = positionals;
2697
+ if (!command || command === "help" || command === "--help" || command === "-h") return printHelp();
2698
+ if (command === "init") return init();
2699
+ if (command === "auth") return auth();
2700
+ if (command === "doctor") return doctor();
2701
+ if (command === "capabilities") return capabilities();
2702
+ if (command === "mcp:install") return mcpInstall();
2703
+ if (command === "agents:install") return agentsInstall();
2704
+ if (command === "profile:list") return profileList();
2705
+ if (command === "profile:use") return profileUse(fileArg);
2706
+ if (command === "store:list") return storeList();
2707
+ if (command === "store:auth") return storeAuth();
2708
+ if (command === "theme:list") return themeList();
2709
+ if (command === "theme:select") return themeSelect();
2710
+ if (command === "theme:check") return themeCommand("check");
2711
+ if (command === "theme:pull") return themeCommand("pull");
2712
+ if (command === "theme:dev") return themeCommand("dev");
2713
+ if (command === "theme:push") return themeCommand("push");
2714
+ if (command === "theme:publish") return themeCommand("publish");
2715
+ if (command === "app:dev") return appCommand("dev");
2716
+ if (command === "app:deploy") return appCommand("deploy");
2717
+ if (command === "admin:query") return adminQuery(fileArg);
2718
+ if (command === "admin:mutate") return adminMutate(fileArg);
2719
+
2720
+ // Products & Catalog
2721
+ if (command === "products:list") return productsList();
2722
+ if (command === "products:get") return productsGet();
2723
+ if (command === "products:create") return productsCreate();
2724
+ if (command === "products:publish") return productsSetStatus("ACTIVE");
2725
+ if (command === "products:archive") return productsSetStatus("ARCHIVED");
2726
+ if (command === "products:draft") return productsSetStatus("DRAFT");
2727
+ if (command === "products:delete") return productsDelete();
2728
+ if (command === "variants:list") return variantsList();
2729
+ if (command === "variants:update") return variantsUpdate();
2730
+ if (command === "collections:list") return collectionsList();
2731
+ if (command === "collections:create") return collectionsCreate();
2732
+
2733
+ // Inventory
2734
+ if (command === "inventory:list") return inventoryList();
2735
+ if (command === "inventory:set") return inventorySet(fileArg);
2736
+ if (command === "inventory:adjust") return inventoryAdjust();
2737
+
2738
+ // Orders
2739
+ if (command === "orders:list") return ordersList();
2740
+ if (command === "orders:get") return ordersGet();
2741
+ if (command === "orders:cancel") return ordersCancel();
2742
+ if (command === "orders:close") return ordersClose();
2743
+
2744
+ // Customers
2745
+ if (command === "customers:list") return customersList();
2746
+ if (command === "customers:search") return customersSearch();
2747
+ if (command === "customers:get") return customersGet();
2748
+ if (command === "customers:create") return customersCreate();
2749
+ if (command === "customers:tags") return customersTags();
2750
+
2751
+ // Discounts & Gift Cards
2752
+ if (command === "discounts:list") return discountsList();
2753
+ if (command === "discounts:create") return discountCreate();
2754
+ if (command === "discounts:delete") return discountsDelete();
2755
+ if (command === "discounts:disable") return discountsDisable();
2756
+ if (command === "discounts:enable") return discountsEnable();
2757
+ if (command === "discount:create") return discountCreate(); // legacy alias
2758
+ if (command === "gift-cards:list") return giftCardsList();
2759
+ if (command === "gift-cards:create") return giftCardsCreate();
2760
+
2761
+ // Store
2762
+ if (command === "store:info") return storeInfo();
2763
+ if (command === "store:locations") return storeLocations();
2764
+ if (command === "store:dashboard") return storeDashboard();
2765
+ if (command === "dashboard") return storeDashboard();
2766
+
2767
+ // Content
2768
+ if (command === "pages:list") return pagesList();
2769
+ if (command === "pages:create") return pagesCreate();
2770
+ if (command === "blogs:list") return blogsList();
2771
+ if (command === "articles:list") return articlesList();
2772
+ if (command === "redirects:list") return redirectsList();
2773
+ if (command === "redirects:create") return redirectsCreate();
2774
+
2775
+ // Metafields
2776
+ if (command === "metafields:list") return metafieldsList();
2777
+ if (command === "metafields:set") return metafieldsSet();
2778
+ console.error(`Unknown command: ${command}`);
2779
+ printHelp();
2780
+ process.exit(1);
2781
+ }
2782
+
2783
+ main().catch((error) => {
2784
+ console.error(error);
2785
+ process.exit(1);
2786
+ });