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.
- package/.cursor/rules/shopify-agentic-dev.mdc +290 -0
- package/AGENTS.md +285 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +285 -0
- package/GEMINI.md +285 -0
- package/HANDOFF.md +351 -0
- package/LICENSE +21 -0
- package/README.md +370 -0
- package/bin/shopify-agent.js +2786 -0
- package/docs/00-prerequisites.md +88 -0
- package/docs/01-onboarding.md +111 -0
- package/docs/02-theme-sdlc.md +106 -0
- package/docs/03-app-sdlc.md +66 -0
- package/docs/04-admin-api-ops.md +58 -0
- package/docs/05-codex-prompts.md +37 -0
- package/docs/06-security-guardrails.md +47 -0
- package/docs/07-github-rollout.md +30 -0
- package/docs/08-product-design.md +168 -0
- package/docs/09-shopify-cli-4-capabilities.md +48 -0
- package/docs/10-field-learnings.md +66 -0
- package/package.json +82 -0
- package/scripts/bootstrap.sh +35 -0
- package/scripts/validate-graphql.js +303 -0
- package/templates/.env.example +20 -0
- package/templates/codex-config.toml +3 -0
- package/templates/github-actions/deploy-theme.yml +32 -0
- package/templates/graphql/content/pages-list.graphql +12 -0
- package/templates/graphql/content/redirects-list.graphql +9 -0
- package/templates/graphql/customers/new-customers.graphql +15 -0
- package/templates/graphql/customers/top-spenders.graphql +16 -0
- package/templates/graphql/discounts/active-discounts.graphql +27 -0
- package/templates/graphql/discounts-list.graphql +40 -0
- package/templates/graphql/inventory-audit.graphql +38 -0
- package/templates/graphql/metafields/product-metafields.graphql +14 -0
- package/templates/graphql/orders/list-open.graphql +30 -0
- package/templates/graphql/orders/revenue-summary.graphql +15 -0
- package/templates/graphql/products/list.graphql +16 -0
- package/templates/graphql/products/low-inventory.graphql +18 -0
- package/templates/graphql/products-seo-audit.graphql +14 -0
- package/templates/graphql/shop-query.graphql +9 -0
- package/templates/graphql/store/full-info.graphql +23 -0
- package/templates/graphql/store/webhooks.graphql +16 -0
- package/templates/prompts/admin-operation.md +12 -0
- 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
|
+
});
|