@cli4ai/shopify 1.0.1 → 1.0.4

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/cli4ai.json CHANGED
@@ -1,45 +1,88 @@
1
1
  {
2
2
  "name": "shopify",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Shopify management tools for orders, products, inventory, and content",
5
5
  "author": "cliforai",
6
- "license": "MIT",
7
- "entry": "run.ts",
8
- "runtime": "bun",
9
- "keywords": ["shopify", "ecommerce", "orders", "inventory", "seo", "blog"],
6
+ "license": "BUSL-1.1",
7
+ "entry": "dist/run.js",
8
+ "runtime": "node",
9
+ "keywords": [
10
+ "shopify",
11
+ "ecommerce",
12
+ "orders",
13
+ "inventory",
14
+ "seo",
15
+ "blog"
16
+ ],
10
17
  "commands": {
11
18
  "orders": {
12
19
  "description": "List recent orders",
13
20
  "args": [
14
- { "name": "limit", "description": "Number of orders (default: 10)", "required": false },
15
- { "name": "status", "description": "Order status (open, closed, any)", "required": false }
21
+ {
22
+ "name": "limit",
23
+ "description": "Number of orders (default: 10)",
24
+ "required": false
25
+ },
26
+ {
27
+ "name": "status",
28
+ "description": "Order status (open, closed, any)",
29
+ "required": false
30
+ }
16
31
  ]
17
32
  },
18
33
  "products": {
19
34
  "description": "List products",
20
35
  "args": [
21
- { "name": "limit", "description": "Number of products (default: 10)", "required": false },
22
- { "name": "query", "description": "Search query", "required": false }
36
+ {
37
+ "name": "limit",
38
+ "description": "Number of products (default: 10)",
39
+ "required": false
40
+ },
41
+ {
42
+ "name": "query",
43
+ "description": "Search query",
44
+ "required": false
45
+ }
23
46
  ]
24
47
  },
25
48
  "inventory": {
26
49
  "description": "Check inventory levels",
27
50
  "args": [
28
- { "name": "ids", "description": "Comma-separated variant IDs", "required": false },
29
- { "name": "limit", "description": "Limit results if no IDs (default: 10)", "required": false }
51
+ {
52
+ "name": "ids",
53
+ "description": "Comma-separated variant IDs",
54
+ "required": false
55
+ },
56
+ {
57
+ "name": "limit",
58
+ "description": "Limit results if no IDs (default: 10)",
59
+ "required": false
60
+ }
30
61
  ]
31
62
  },
32
63
  "reports": {
33
64
  "description": "Get sales summary",
34
65
  "args": [
35
- { "name": "since", "description": "Start date (YYYY-MM-DD)", "required": false }
66
+ {
67
+ "name": "since",
68
+ "description": "Start date (YYYY-MM-DD)",
69
+ "required": false
70
+ }
36
71
  ]
37
72
  },
38
73
  "seo_audit": {
39
74
  "description": "Check SEO data for a resource",
40
75
  "args": [
41
- { "name": "handle", "description": "Resource handle", "required": true },
42
- { "name": "type", "description": "Resource type (products, pages, articles)", "required": true }
76
+ {
77
+ "name": "handle",
78
+ "description": "Resource handle",
79
+ "required": true
80
+ },
81
+ {
82
+ "name": "type",
83
+ "description": "Resource type (products, pages, articles)",
84
+ "required": true
85
+ }
43
86
  ]
44
87
  },
45
88
  "blogs": {
@@ -49,24 +92,58 @@
49
92
  "articles": {
50
93
  "description": "List articles in a blog",
51
94
  "args": [
52
- { "name": "blog_id", "description": "ID of the blog", "required": true },
53
- { "name": "limit", "description": "Number of articles (default: 10)", "required": false }
95
+ {
96
+ "name": "blog_id",
97
+ "description": "ID of the blog",
98
+ "required": true
99
+ },
100
+ {
101
+ "name": "limit",
102
+ "description": "Number of articles (default: 10)",
103
+ "required": false
104
+ }
54
105
  ]
55
106
  },
56
107
  "article_create": {
57
108
  "description": "Create a new article",
58
109
  "args": [
59
- { "name": "blog_id", "description": "ID of the blog", "required": true },
60
- { "name": "title", "description": "Article title", "required": true },
61
- { "name": "author", "description": "Author name", "required": true },
62
- { "name": "tags", "description": "Comma-separated tags", "required": false },
63
- { "name": "summary_html", "description": "Summary HTML", "required": false }
110
+ {
111
+ "name": "blog_id",
112
+ "description": "ID of the blog",
113
+ "required": true
114
+ },
115
+ {
116
+ "name": "title",
117
+ "description": "Article title",
118
+ "required": true
119
+ },
120
+ {
121
+ "name": "author",
122
+ "description": "Author name",
123
+ "required": true
124
+ },
125
+ {
126
+ "name": "tags",
127
+ "description": "Comma-separated tags",
128
+ "required": false
129
+ },
130
+ {
131
+ "name": "summary_html",
132
+ "description": "Summary HTML",
133
+ "required": false
134
+ }
64
135
  ]
65
136
  }
66
137
  },
67
138
  "env": {
68
- "SHOPIFY_SHOP": { "required": true, "description": "Shopify shop domain (e.g. my-shop.myshopify.com)" },
69
- "SHOPIFY_ACCESS_TOKEN": { "required": true, "description": "Admin API Access Token" }
139
+ "SHOPIFY_SHOP": {
140
+ "required": true,
141
+ "description": "Shopify shop domain (e.g. my-shop.myshopify.com)"
142
+ },
143
+ "SHOPIFY_ACCESS_TOKEN": {
144
+ "required": true,
145
+ "description": "Admin API Access Token"
146
+ }
70
147
  },
71
148
  "dependencies": {
72
149
  "@cli4ai/lib": "^1.0.0",
package/dist/run.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/run.js ADDED
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ import { cli, output, withErrorHandling } from '@cli4ai/lib';
3
+ const SHOP = process.env.SHOPIFY_SHOP;
4
+ const TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
5
+ const API_VERSION = '2024-01';
6
+ if (!SHOP || !TOKEN) {
7
+ // We rely on the MCP server or user to provide these
8
+ // But for direct CLI usage, we warn if missing, though @cli4ai/lib might handle envs?
9
+ // Actually, standard practice here is to fail if they are needed and not present.
10
+ // But we will let the individual commands fail or check at runtime.
11
+ }
12
+ async function shopifyFetch(endpoint, options = {}) {
13
+ if (!SHOP || !TOKEN) {
14
+ throw new Error('Missing SHOPIFY_SHOP or SHOPIFY_ACCESS_TOKEN environment variables');
15
+ }
16
+ const url = `https://${SHOP}/admin/api/${API_VERSION}/${endpoint}`;
17
+ const response = await fetch(url, {
18
+ ...options,
19
+ headers: {
20
+ 'X-Shopify-Access-Token': TOKEN,
21
+ 'Content-Type': 'application/json',
22
+ ...options.headers,
23
+ },
24
+ });
25
+ if (!response.ok) {
26
+ const errorText = await response.text();
27
+ throw new Error(`Shopify API Error (${response.status}): ${errorText}`);
28
+ }
29
+ return response.json();
30
+ }
31
+ const program = cli('shopify', '1.0.0', 'Shopify CLI wrapper');
32
+ program
33
+ .command('orders')
34
+ .description('List recent orders')
35
+ .option('-l, --limit <number>', 'Number of orders', '10')
36
+ .option('-s, --status <status>', 'Order status', 'any')
37
+ .action(withErrorHandling(async (options) => {
38
+ const data = await shopifyFetch(`orders.json?limit=${options.limit}&status=${options.status}`);
39
+ const formatted = data.orders.map(o => ({
40
+ id: o.id,
41
+ order_number: o.order_number,
42
+ created_at: o.created_at,
43
+ total_price: o.total_price,
44
+ currency: o.currency,
45
+ customer: o.customer ? `${o.customer.first_name} ${o.customer.last_name}` : 'No Customer',
46
+ financial_status: o.financial_status,
47
+ fulfillment_status: o.fulfillment_status
48
+ }));
49
+ output(formatted);
50
+ }));
51
+ program
52
+ .command('products')
53
+ .description('List products')
54
+ .option('-l, --limit <number>', 'Number of products', '10')
55
+ .option('-q, --query <query>', 'Search query') // Basic title search if needed, or just list
56
+ .action(withErrorHandling(async (options) => {
57
+ let url = `products.json?limit=${options.limit}`;
58
+ if (options.query) {
59
+ // Shopify products endpoint doesn't support generic 'query' well in REST without specific fields
60
+ // but we can use 'title' if intended, or just fetch and filter.
61
+ // Better: Just fetch recent. If query is needed, use GraphQL or other endpoints.
62
+ // For now, ignoring query in REST url unless we map it to title or similar.
63
+ // Actually, standard REST has no fuzzy search. We'll just list.
64
+ // Or we can rely on `title` param if exact match? No.
65
+ // Let's just list recent.
66
+ if (options.query)
67
+ console.warn('Note: "query" parameter is limited in REST API, listing recent products.');
68
+ }
69
+ const data = await shopifyFetch(url);
70
+ const formatted = data.products.map(p => ({
71
+ id: p.id,
72
+ title: p.title,
73
+ handle: p.handle,
74
+ status: p.status,
75
+ variants: p.variants.length,
76
+ inventory: p.variants.reduce((sum, v) => sum + (v.inventory_quantity || 0), 0)
77
+ }));
78
+ output(formatted);
79
+ }));
80
+ program
81
+ .command('inventory')
82
+ .description('Check inventory')
83
+ .option('-i, --ids <ids>', 'Variant IDs')
84
+ .option('-l, --limit <number>', 'Limit', '10')
85
+ .action(withErrorHandling(async (options) => {
86
+ if (options.ids) {
87
+ const data = await shopifyFetch(`variants.json?ids=${options.ids}`);
88
+ output(data.variants.map(v => ({
89
+ id: v.id,
90
+ product_id: v.product_id,
91
+ title: v.title, // Variant title
92
+ inventory: v.inventory_quantity
93
+ })));
94
+ }
95
+ else {
96
+ // Fetch products and show inventory
97
+ const data = await shopifyFetch(`products.json?limit=${options.limit}`);
98
+ const inventory = data.products.flatMap(p => p.variants.map((v) => ({
99
+ product: p.title,
100
+ variant: v.title,
101
+ sku: v.sku,
102
+ inventory: v.inventory_quantity
103
+ })));
104
+ output(inventory);
105
+ }
106
+ }));
107
+ program
108
+ .command('reports')
109
+ .description('Sales summary')
110
+ .option('--since <date>', 'Start date YYYY-MM-DD')
111
+ .action(withErrorHandling(async (options) => {
112
+ let url = 'orders.json?status=any&limit=250'; // 250 is max
113
+ if (options.since) {
114
+ url += `&created_at_min=${options.since}T00:00:00`;
115
+ }
116
+ const data = await shopifyFetch(url);
117
+ let totalSales = 0;
118
+ let orderCount = data.orders.length;
119
+ let currency = data.orders[0]?.currency || 'USD';
120
+ for (const order of data.orders) {
121
+ // Very basic: sum total_price of valid orders
122
+ // Exclude cancelled/voided if strictly sales?
123
+ // For summary, let's just sum total_price.
124
+ totalSales += parseFloat(order.total_price);
125
+ }
126
+ output({
127
+ period: options.since ? `Since ${options.since}` : 'Recent 250 orders',
128
+ order_count: orderCount,
129
+ total_sales: totalSales.toFixed(2),
130
+ currency
131
+ });
132
+ }));
133
+ program
134
+ .command('seo_audit')
135
+ .description('Check SEO data')
136
+ .argument('<handle>', 'Resource handle')
137
+ .argument('<type>', 'Resource type (products, pages, articles)')
138
+ .action(withErrorHandling(async (handle, type) => {
139
+ // We need to find ID from handle? REST API often requires ID.
140
+ // Except `products` can be filtered by handle? No, REST `products` endpoint filters are limited.
141
+ // But `products.json?handle=...` exists? No.
142
+ // Wait, checking Shopify API docs... `GET /admin/api/2024-01/products.json?handle=...` DOES work to find by handle.
143
+ let endpoint = '';
144
+ let resourceKey = '';
145
+ if (type === 'products' || type === 'product') {
146
+ endpoint = `products.json?handle=${handle}`;
147
+ resourceKey = 'products';
148
+ }
149
+ else if (type === 'pages' || type === 'page') {
150
+ endpoint = `pages.json?handle=${handle}`;
151
+ resourceKey = 'pages';
152
+ }
153
+ else if (type === 'articles' || type === 'article') {
154
+ // Articles need blog_id usually, handle is not unique globally?
155
+ // Actually `articles` endpoint exists but scoping is tricky.
156
+ // Let's assume user provides handle, but finding it might be hard without blog_id.
157
+ // For simplicity, let's fail or ask for blog_id.
158
+ // Or simpler: just support products/pages for now as they are global.
159
+ // But wait, user asked for "seo".
160
+ throw new Error('For articles, use "articles" command to find ID then inspect. "seo_audit" currently supports: products, pages.');
161
+ }
162
+ else {
163
+ throw new Error('Unknown type. Use: products, pages');
164
+ }
165
+ const data = await shopifyFetch(endpoint);
166
+ const resources = data[resourceKey];
167
+ if (!resources || resources.length === 0) {
168
+ throw new Error(`No ${type} found with handle "${handle}"`);
169
+ }
170
+ const item = resources[0];
171
+ // Analyze
172
+ const analysis = {
173
+ id: item.id,
174
+ title: item.title,
175
+ handle: item.handle,
176
+ meta_title: item.metafields_global_title_tag || 'Not set (uses title)',
177
+ meta_description: item.metafields_global_description_tag || 'Not set (uses body excerpt)',
178
+ body_length: item.body_html ? item.body_html.length : 0,
179
+ created_at: item.created_at,
180
+ warnings: []
181
+ };
182
+ if (!analysis.meta_title)
183
+ analysis.warnings.push('Missing explicit meta title');
184
+ if (!analysis.meta_description)
185
+ analysis.warnings.push('Missing explicit meta description');
186
+ if (analysis.title.length > 70)
187
+ analysis.warnings.push('Title too long (>70 chars)');
188
+ output(analysis);
189
+ }));
190
+ program
191
+ .command('blogs')
192
+ .description('List blogs')
193
+ .action(withErrorHandling(async () => {
194
+ const data = await shopifyFetch('blogs.json');
195
+ output(data.blogs.map(b => ({
196
+ id: b.id,
197
+ title: b.title,
198
+ handle: b.handle,
199
+ commentable: b.commentable
200
+ })));
201
+ }));
202
+ program
203
+ .command('articles')
204
+ .description('List articles')
205
+ .argument('<blog_id>', 'Blog ID')
206
+ .option('-l, --limit <number>', 'Limit', '10')
207
+ .action(withErrorHandling(async (blogId, options) => {
208
+ const data = await shopifyFetch(`blogs/${blogId}/articles.json?limit=${options.limit}`);
209
+ output(data.articles.map(a => ({
210
+ id: a.id,
211
+ title: a.title,
212
+ author: a.author,
213
+ created_at: a.created_at,
214
+ tags: a.tags,
215
+ published: a.published
216
+ })));
217
+ }));
218
+ program
219
+ .command('article_create')
220
+ .description('Create article')
221
+ .argument('<blog_id>', 'Blog ID')
222
+ .argument('<title>', 'Title')
223
+ .argument('<author>', 'Author')
224
+ .argument('[tags]', 'Tags (comma separated)')
225
+ .argument('[summary_html]', 'Summary HTML')
226
+ .action(withErrorHandling(async (blogId, title, author, tags, summaryHtml) => {
227
+ // We create a basic article.
228
+ // Note: User might want to set body_html.
229
+ // Since args are limited in CLI, maybe we just set summary or basic body.
230
+ // Real usage would likely involve piping content or a more complex object.
231
+ // For now, we set body_html = summary_html to ensure it has content if provided.
232
+ const payload = {
233
+ article: {
234
+ title,
235
+ author,
236
+ tags: tags || '',
237
+ summary_html: summaryHtml || '',
238
+ body_html: summaryHtml || '<p>Content pending...</p>',
239
+ published: true
240
+ }
241
+ };
242
+ const data = await shopifyFetch(`blogs/${blogId}/articles.json`, {
243
+ method: 'POST',
244
+ body: JSON.stringify(payload)
245
+ });
246
+ output({
247
+ id: data.article.id,
248
+ title: data.article.title,
249
+ url: `.../blogs/${blogId}/${data.article.id}`, // Construct url if possible
250
+ status: 'Created'
251
+ });
252
+ }));
253
+ program.parse();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cli4ai/shopify",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Shopify management tools for orders, products, inventory, and content",
5
5
  "author": "cliforai",
6
- "license": "MIT",
7
- "main": "run.ts",
6
+ "license": "BUSL-1.1",
7
+ "main": "dist/run.js",
8
8
  "bin": {
9
- "shopify": "./run.ts"
9
+ "shopify": "./dist/run.js"
10
10
  },
11
11
  "type": "module",
12
12
  "keywords": [
@@ -34,12 +34,20 @@
34
34
  "commander": "^14.0.0"
35
35
  },
36
36
  "files": [
37
- "run.ts",
37
+ "dist",
38
38
  "cli4ai.json",
39
39
  "README.md",
40
40
  "LICENSE"
41
41
  ],
42
42
  "publishConfig": {
43
43
  "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^5.0.0",
51
+ "@types/node": "^22.0.0"
44
52
  }
45
53
  }
package/run.ts DELETED
@@ -1,277 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { cli, output, outputError, withErrorHandling, parseJson } from '@cli4ai/lib/cli.ts';
3
-
4
- const SHOP = process.env.SHOPIFY_SHOP;
5
- const TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
6
- const API_VERSION = '2024-01';
7
-
8
- if (!SHOP || !TOKEN) {
9
- // We rely on the MCP server or user to provide these
10
- // But for direct CLI usage, we warn if missing, though @cli4ai/lib might handle envs?
11
- // Actually, standard practice here is to fail if they are needed and not present.
12
- // But we will let the individual commands fail or check at runtime.
13
- }
14
-
15
- async function shopifyFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
16
- if (!SHOP || !TOKEN) {
17
- throw new Error('Missing SHOPIFY_SHOP or SHOPIFY_ACCESS_TOKEN environment variables');
18
- }
19
-
20
- const url = `https://${SHOP}/admin/api/${API_VERSION}/${endpoint}`;
21
- const response = await fetch(url, {
22
- ...options,
23
- headers: {
24
- 'X-Shopify-Access-Token': TOKEN,
25
- 'Content-Type': 'application/json',
26
- ...options.headers,
27
- },
28
- });
29
-
30
- if (!response.ok) {
31
- const errorText = await response.text();
32
- throw new Error(`Shopify API Error (${response.status}): ${errorText}`);
33
- }
34
-
35
- return response.json() as Promise<T>;
36
- }
37
-
38
- const program = cli('shopify', '1.0.0', 'Shopify CLI wrapper');
39
-
40
- program
41
- .command('orders')
42
- .description('List recent orders')
43
- .option('-l, --limit <number>', 'Number of orders', '10')
44
- .option('-s, --status <status>', 'Order status', 'any')
45
- .action(withErrorHandling(async (options: { limit: string; status: string }) => {
46
- const data = await shopifyFetch<{ orders: any[] }>(`orders.json?limit=${options.limit}&status=${options.status}`);
47
- const formatted = data.orders.map(o => ({
48
- id: o.id,
49
- order_number: o.order_number,
50
- created_at: o.created_at,
51
- total_price: o.total_price,
52
- currency: o.currency,
53
- customer: o.customer ? `${o.customer.first_name} ${o.customer.last_name}` : 'No Customer',
54
- financial_status: o.financial_status,
55
- fulfillment_status: o.fulfillment_status
56
- }));
57
- output(formatted);
58
- }));
59
-
60
- program
61
- .command('products')
62
- .description('List products')
63
- .option('-l, --limit <number>', 'Number of products', '10')
64
- .option('-q, --query <query>', 'Search query') // Basic title search if needed, or just list
65
- .action(withErrorHandling(async (options: { limit: string; query?: string }) => {
66
- let url = `products.json?limit=${options.limit}`;
67
- if (options.query) {
68
- // Shopify products endpoint doesn't support generic 'query' well in REST without specific fields
69
- // but we can use 'title' if intended, or just fetch and filter.
70
- // Better: Just fetch recent. If query is needed, use GraphQL or other endpoints.
71
- // For now, ignoring query in REST url unless we map it to title or similar.
72
- // Actually, standard REST has no fuzzy search. We'll just list.
73
- // Or we can rely on `title` param if exact match? No.
74
- // Let's just list recent.
75
- if (options.query) console.warn('Note: "query" parameter is limited in REST API, listing recent products.');
76
- }
77
-
78
- const data = await shopifyFetch<{ products: any[] }>(url);
79
- const formatted = data.products.map(p => ({
80
- id: p.id,
81
- title: p.title,
82
- handle: p.handle,
83
- status: p.status,
84
- variants: p.variants.length,
85
- inventory: p.variants.reduce((sum: number, v: any) => sum + (v.inventory_quantity || 0), 0)
86
- }));
87
- output(formatted);
88
- }));
89
-
90
- program
91
- .command('inventory')
92
- .description('Check inventory')
93
- .option('-i, --ids <ids>', 'Variant IDs')
94
- .option('-l, --limit <number>', 'Limit', '10')
95
- .action(withErrorHandling(async (options: { ids?: string; limit: string }) => {
96
- if (options.ids) {
97
- const data = await shopifyFetch<{ variants: any[] }>(`variants.json?ids=${options.ids}`);
98
- output(data.variants.map(v => ({
99
- id: v.id,
100
- product_id: v.product_id,
101
- title: v.title, // Variant title
102
- inventory: v.inventory_quantity
103
- })));
104
- } else {
105
- // Fetch products and show inventory
106
- const data = await shopifyFetch<{ products: any[] }>(`products.json?limit=${options.limit}`);
107
- const inventory = data.products.flatMap(p => p.variants.map((v: any) => ({
108
- product: p.title,
109
- variant: v.title,
110
- sku: v.sku,
111
- inventory: v.inventory_quantity
112
- })));
113
- output(inventory);
114
- }
115
- }));
116
-
117
- program
118
- .command('reports')
119
- .description('Sales summary')
120
- .option('--since <date>', 'Start date YYYY-MM-DD')
121
- .action(withErrorHandling(async (options: { since?: string }) => {
122
- let url = 'orders.json?status=any&limit=250'; // 250 is max
123
- if (options.since) {
124
- url += `&created_at_min=${options.since}T00:00:00`;
125
- }
126
-
127
- const data = await shopifyFetch<{ orders: any[] }>(url);
128
-
129
- let totalSales = 0;
130
- let orderCount = data.orders.length;
131
- let currency = data.orders[0]?.currency || 'USD';
132
-
133
- for (const order of data.orders) {
134
- // Very basic: sum total_price of valid orders
135
- // Exclude cancelled/voided if strictly sales?
136
- // For summary, let's just sum total_price.
137
- totalSales += parseFloat(order.total_price);
138
- }
139
-
140
- output({
141
- period: options.since ? `Since ${options.since}` : 'Recent 250 orders',
142
- order_count: orderCount,
143
- total_sales: totalSales.toFixed(2),
144
- currency
145
- });
146
- }));
147
-
148
- program
149
- .command('seo_audit')
150
- .description('Check SEO data')
151
- .argument('<handle>', 'Resource handle')
152
- .argument('<type>', 'Resource type (products, pages, articles)')
153
- .action(withErrorHandling(async (handle: string, type: string) => {
154
- // We need to find ID from handle? REST API often requires ID.
155
- // Except `products` can be filtered by handle? No, REST `products` endpoint filters are limited.
156
- // But `products.json?handle=...` exists? No.
157
- // Wait, checking Shopify API docs... `GET /admin/api/2024-01/products.json?handle=...` DOES work to find by handle.
158
-
159
- let endpoint = '';
160
- let resourceKey = '';
161
-
162
- if (type === 'products' || type === 'product') {
163
- endpoint = `products.json?handle=${handle}`;
164
- resourceKey = 'products';
165
- } else if (type === 'pages' || type === 'page') {
166
- endpoint = `pages.json?handle=${handle}`;
167
- resourceKey = 'pages';
168
- } else if (type === 'articles' || type === 'article') {
169
- // Articles need blog_id usually, handle is not unique globally?
170
- // Actually `articles` endpoint exists but scoping is tricky.
171
- // Let's assume user provides handle, but finding it might be hard without blog_id.
172
- // For simplicity, let's fail or ask for blog_id.
173
- // Or simpler: just support products/pages for now as they are global.
174
- // But wait, user asked for "seo".
175
- throw new Error('For articles, use "articles" command to find ID then inspect. "seo_audit" currently supports: products, pages.');
176
- } else {
177
- throw new Error('Unknown type. Use: products, pages');
178
- }
179
-
180
- const data = await shopifyFetch<any>(endpoint);
181
- const resources = data[resourceKey];
182
-
183
- if (!resources || resources.length === 0) {
184
- throw new Error(`No ${type} found with handle "${handle}"`);
185
- }
186
-
187
- const item = resources[0];
188
-
189
- // Analyze
190
- const analysis = {
191
- id: item.id,
192
- title: item.title,
193
- handle: item.handle,
194
- meta_title: item.metafields_global_title_tag || 'Not set (uses title)',
195
- meta_description: item.metafields_global_description_tag || 'Not set (uses body excerpt)',
196
- body_length: item.body_html ? item.body_html.length : 0,
197
- created_at: item.created_at,
198
- warnings: [] as string[]
199
- };
200
-
201
- if (!analysis.meta_title) analysis.warnings.push('Missing explicit meta title');
202
- if (!analysis.meta_description) analysis.warnings.push('Missing explicit meta description');
203
- if (analysis.title.length > 70) analysis.warnings.push('Title too long (>70 chars)');
204
-
205
- output(analysis);
206
- }));
207
-
208
- program
209
- .command('blogs')
210
- .description('List blogs')
211
- .action(withErrorHandling(async () => {
212
- const data = await shopifyFetch<{ blogs: any[] }>('blogs.json');
213
- output(data.blogs.map(b => ({
214
- id: b.id,
215
- title: b.title,
216
- handle: b.handle,
217
- commentable: b.commentable
218
- })));
219
- }));
220
-
221
- program
222
- .command('articles')
223
- .description('List articles')
224
- .argument('<blog_id>', 'Blog ID')
225
- .option('-l, --limit <number>', 'Limit', '10')
226
- .action(withErrorHandling(async (blogId: string, options: { limit: string }) => {
227
- const data = await shopifyFetch<{ articles: any[] }>(`blogs/${blogId}/articles.json?limit=${options.limit}`);
228
- output(data.articles.map(a => ({
229
- id: a.id,
230
- title: a.title,
231
- author: a.author,
232
- created_at: a.created_at,
233
- tags: a.tags,
234
- published: a.published
235
- })));
236
- }));
237
-
238
- program
239
- .command('article_create')
240
- .description('Create article')
241
- .argument('<blog_id>', 'Blog ID')
242
- .argument('<title>', 'Title')
243
- .argument('<author>', 'Author')
244
- .argument('[tags]', 'Tags (comma separated)')
245
- .argument('[summary_html]', 'Summary HTML')
246
- .action(withErrorHandling(async (blogId: string, title: string, author: string, tags?: string, summaryHtml?: string) => {
247
- // We create a basic article.
248
- // Note: User might want to set body_html.
249
- // Since args are limited in CLI, maybe we just set summary or basic body.
250
- // Real usage would likely involve piping content or a more complex object.
251
- // For now, we set body_html = summary_html to ensure it has content if provided.
252
-
253
- const payload = {
254
- article: {
255
- title,
256
- author,
257
- tags: tags || '',
258
- summary_html: summaryHtml || '',
259
- body_html: summaryHtml || '<p>Content pending...</p>',
260
- published: true
261
- }
262
- };
263
-
264
- const data = await shopifyFetch<{ article: any }>(`blogs/${blogId}/articles.json`, {
265
- method: 'POST',
266
- body: JSON.stringify(payload)
267
- });
268
-
269
- output({
270
- id: data.article.id,
271
- title: data.article.title,
272
- url: `.../blogs/${blogId}/${data.article.id}`, // Construct url if possible
273
- status: 'Created'
274
- });
275
- }));
276
-
277
- program.parse();