@o2vend/theme-cli 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * WebstoreAPI Fetcher (Real Mode)
3
+ * Thin HTTP client for public /webstoreapi/* endpoints only.
4
+ * No webstore internals, business logic, or service abstractions.
5
+ */
6
+
7
+ const axios = require('axios');
8
+
9
+ const DEFAULT_SECTIONS = ['hero', 'products', 'footer', 'content', 'header'];
10
+ const TIMEOUT = parseInt(process.env.O2VEND_API_TIMEOUT, 10) || 10000;
11
+ const DEBUG = process.env.DEBUG_WEBSTOREAPI === 'true' || process.env.DEBUG_WEBSTOREAPI === '1';
12
+
13
+ function logRequest(method, path, status, extra = {}) {
14
+ const msg = `[webstoreapi] ${method} ${path} ${status}`;
15
+ if (DEBUG || status >= 400) {
16
+ const parts = [msg];
17
+ if (extra.bodySnippet) parts.push(extra.bodySnippet);
18
+ if (extra.headers) parts.push(JSON.stringify(extra.headers));
19
+ console.log(parts.join(' '));
20
+ }
21
+ }
22
+
23
+ function truncate(v, max = 300) {
24
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
25
+ return s.length <= max ? s : s.slice(0, max) + '...';
26
+ }
27
+
28
+ /**
29
+ * Derive webstoreapi base URL from O2VEND_API_BASE_URL.
30
+ * Strips /shopfront/api/v2 if present.
31
+ * @returns {string} e.g. https://sareesdemo.myo2vend.com/webstoreapi
32
+ */
33
+ function getWebstoreapiBaseUrl() {
34
+ let base = process.env.O2VEND_API_BASE_URL || '';
35
+ base = base.replace(/\/$/, '');
36
+ base = base.replace(/\/shopfront\/api\/v2\/?$/i, '');
37
+ base = base.replace(/\/webstoreapi\/?$/i, '');
38
+ return base ? `${base}/webstoreapi` : '';
39
+ }
40
+
41
+ /**
42
+ * Derive shopfront API base URL from O2VEND_API_BASE_URL.
43
+ * Used only for GET /storeinfo (store logo, favicon, name).
44
+ * @returns {string} e.g. https://sareesdemo.myo2vend.com/shopfront/api/v2
45
+ */
46
+ function getShopfrontBaseUrl() {
47
+ let base = process.env.O2VEND_API_BASE_URL || '';
48
+ base = base.replace(/\/$/, '');
49
+ base = base.replace(/\/webstoreapi\/?$/i, '');
50
+ if (/\/shopfront\/api\/v2\/?$/i.test(base)) return base.replace(/\/$/, '');
51
+ base = base.replace(/\/shopfront\/api\/v2\/?$/i, '');
52
+ return base ? `${base}/shopfront/api/v2` : '';
53
+ }
54
+
55
+ /**
56
+ * Fetch store info from shopfront API (GET /storeinfo).
57
+ * Used in real mode for shop.logo, shop.favicon, shop.name.
58
+ * Requires O2VEND_API_KEY. On failure, returns null.
59
+ * @returns {Promise<{ logoUrl?: string, favouriteIconUrl?: string, name?: string, settings?: object } | null>}
60
+ */
61
+ async function fetchStoreInfo() {
62
+ const base = getShopfrontBaseUrl();
63
+ const apiKey = process.env.O2VEND_API_KEY;
64
+ if (!base || !apiKey) return null;
65
+ const client = axios.create({
66
+ baseURL: base,
67
+ timeout: TIMEOUT,
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ 'X-O2VEND-SHOPFRONT-API-KEY': apiKey,
71
+ 'User-Agent': 'O2VEND-Theme-CLI/1.0'
72
+ }
73
+ });
74
+ try {
75
+ const res = await client.get('/storeinfo');
76
+ const data = res.data || {};
77
+ const logoUrl = data.logoUrl ?? data.logourl;
78
+ const favouriteIconUrl = data.favouriteIconUrl ?? data.favouriteiconurl;
79
+ const name = data.name ?? data.Name;
80
+ const settings = data.settings || data.Settings || {};
81
+ return {
82
+ logoUrl: logoUrl || null,
83
+ favouriteIconUrl: favouriteIconUrl || null,
84
+ name: name || null,
85
+ settings
86
+ };
87
+ } catch (e) {
88
+ if (DEBUG) console.log('[webstoreapi] GET /storeinfo failed:', e.response?.status || e.message);
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Create axios instance for webstoreapi calls.
95
+ * @param {Object} [opts] - { headers: { Cookie } }
96
+ * @returns {{ baseUrl: string, get: Function, post: Function }}
97
+ */
98
+ function createFetcher(opts = {}) {
99
+ const baseUrl = getWebstoreapiBaseUrl();
100
+ const client = axios.create({
101
+ baseURL: baseUrl,
102
+ timeout: TIMEOUT,
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ 'User-Agent': 'O2VEND-Theme-CLI/1.0',
106
+ ...(opts.headers || {})
107
+ }
108
+ });
109
+
110
+ client.interceptors.response.use(
111
+ (r) => {
112
+ const method = (r.config?.method || 'get').toUpperCase();
113
+ const path = r.config?.url || r.config?.baseURL || '';
114
+ logRequest(method, path, r.status);
115
+ return r;
116
+ },
117
+ (err) => {
118
+ const method = (err.config?.method || 'get').toUpperCase();
119
+ const path = err.config?.url || err.config?.baseURL || '';
120
+ const status = err.response?.status;
121
+ const data = err.response?.data;
122
+ const bodySnippet = data != null ? truncate(data) : err.message;
123
+ logRequest(method, path, status || 'ERR', { bodySnippet });
124
+ if (status >= 400 && typeof data === 'object' && data.message) {
125
+ console.log(`[webstoreapi] error message: ${data.message}`);
126
+ }
127
+ return Promise.reject(err);
128
+ }
129
+ );
130
+
131
+ return {
132
+ baseUrl,
133
+ get: (path, config = {}) => client.get(path, config).then((r) => r.data),
134
+ post: (path, body, config = {}) => client.post(path, body, config).then((r) => r.data)
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Convert products query (POST-style) to query string for GET /webstoreapi/products.
140
+ * @param {Object} p - { limit, offset, filter, searchText, ... }
141
+ * @returns {string}
142
+ */
143
+ function productsQueryToParams(p) {
144
+ const q = new URLSearchParams();
145
+ const limit = p.limit != null ? parseInt(p.limit, 10) : 20;
146
+ const page = p.page != null ? parseInt(p.page, 10) : (p.offset != null ? Math.floor(parseInt(p.offset, 10) / limit) + 1 : 1);
147
+ q.set('page', String(Math.max(1, page)));
148
+ q.set('limit', String(Math.max(1, limit)));
149
+ if (p.searchText) q.set('searchText', p.searchText);
150
+ const cat = p.categoryId || (p.filter && p.filter.category) || '';
151
+ const brand = p.brandId || (p.filter && p.filter.brand) || '';
152
+ q.set('filter[category]', String(cat));
153
+ q.set('filter[brand]', String(brand));
154
+ if (p.sort && typeof p.sort === 'object' && p.sort.field) {
155
+ q.set('sort[field]', p.sort.field);
156
+ }
157
+ return q.toString();
158
+ }
159
+
160
+ /**
161
+ * Convert categories query to query string for GET /webstoreapi/categories.
162
+ * @param {Object} p - { limit, offset }
163
+ * @returns {string}
164
+ */
165
+ function categoriesQueryToParams(p) {
166
+ const q = new URLSearchParams();
167
+ if (p.limit != null) q.set('limit', String(p.limit));
168
+ if (p.offset != null) q.set('offset', String(p.offset));
169
+ return q.toString();
170
+ }
171
+
172
+ /**
173
+ * Fetch products. GET /webstoreapi/products?...
174
+ * @param {Object} params - { limit, offset, filter, searchText, ... }
175
+ * @param {Object} [opts] - { headers: { Cookie } }
176
+ * @returns {Promise<{ products?: Array, Products?: Array, totalCount?: number, TotalCount?: number }>}
177
+ */
178
+ async function fetchProducts(params = {}, opts = {}) {
179
+ const { get } = createFetcher(opts);
180
+ const qs = productsQueryToParams(params);
181
+ const path = qs ? `/products?${qs}` : '/products';
182
+ return get(path);
183
+ }
184
+
185
+ /**
186
+ * Fetch product by ID. GET /webstoreapi/products/:productId
187
+ * @param {string} id
188
+ * @param {Object} [opts]
189
+ * @returns {Promise<Object>}
190
+ */
191
+ async function fetchProductById(id, opts = {}) {
192
+ const { get } = createFetcher(opts);
193
+ return get(`/products/${encodeURIComponent(id)}`);
194
+ }
195
+
196
+ /**
197
+ * Fetch categories. GET /webstoreapi/categories?...
198
+ * @param {Object} params - { limit, offset }
199
+ * @param {Object} [opts]
200
+ * @returns {Promise<{ categories?: Array, data?: { categories?: Array } }>}
201
+ */
202
+ async function fetchCategories(params = {}, opts = {}) {
203
+ const { get } = createFetcher(opts);
204
+ const qs = categoriesQueryToParams(params);
205
+ const path = qs ? `/categories?${qs}` : '/categories';
206
+ return get(path);
207
+ }
208
+
209
+ /**
210
+ * Fetch brands. POST /webstoreapi/brands with body.
211
+ * @param {Object} params - { limit, offset }
212
+ * @param {Object} [opts]
213
+ * @returns {Promise<{ brands?: Array, data?: { brands?: Array } }>}
214
+ */
215
+ async function fetchBrands(params = {}, opts = {}) {
216
+ const { post } = createFetcher(opts);
217
+ return post('/brands', params || {});
218
+ }
219
+
220
+ /**
221
+ * Fetch menus. GET /webstoreapi/menus
222
+ * @param {Object} [opts]
223
+ * @returns {Promise<Array|{ data?: { menus?: Array }, menus?: Array }>}
224
+ */
225
+ async function fetchMenus(opts = {}) {
226
+ const { get } = createFetcher(opts);
227
+ const data = await get('/menus');
228
+ if (Array.isArray(data)) return data;
229
+ return data?.data?.menus || data?.menus || [];
230
+ }
231
+
232
+ /**
233
+ * Fetch menu by ID. GET /webstoreapi/menus/:id
234
+ * @param {string} id
235
+ * @param {Object} [opts]
236
+ * @returns {Promise<Object>}
237
+ */
238
+ async function fetchMenuById(id, opts = {}) {
239
+ const { get } = createFetcher(opts);
240
+ return get(`/menus/${encodeURIComponent(id)}`);
241
+ }
242
+
243
+ /**
244
+ * Fetch cart. GET /webstoreapi/carts (session-based; pass Cookie).
245
+ * @param {Object} [opts] - { headers: { Cookie: req.headers.cookie } }
246
+ * @returns {Promise<{ data?: Object }>}
247
+ */
248
+ async function fetchCart(opts = {}) {
249
+ const { get } = createFetcher(opts);
250
+ return get('/carts');
251
+ }
252
+
253
+ /**
254
+ * Fetch widgets for a section. GET /webstoreapi/sections/widgets/:page/:section
255
+ * @param {string} page - e.g. 'home'
256
+ * @param {string} section - e.g. 'hero', 'products'
257
+ * @param {Object} [opts]
258
+ * @returns {Promise<{ widgets?: Array }>}
259
+ */
260
+ async function fetchSectionsWidgets(page, section, opts = {}) {
261
+ const { get } = createFetcher(opts);
262
+ const data = await get(`/sections/widgets/${encodeURIComponent(page)}/${encodeURIComponent(section)}`);
263
+ const list = Array.isArray(data?.widgets) ? data.widgets : [];
264
+ return { widgets: list };
265
+ }
266
+
267
+ /**
268
+ * Fetch all sections widgets and merge into { hero: [], products: [], ... }.
269
+ * @param {string} [page] - e.g. 'home'
270
+ * @param {string[]} [sections]
271
+ * @param {Object} [opts]
272
+ * @returns {Promise<Object>}
273
+ */
274
+ async function fetchWidgetsBySections(page = 'home', sections = DEFAULT_SECTIONS, opts = {}) {
275
+ const out = {};
276
+ for (const s of sections) {
277
+ out[s] = [];
278
+ }
279
+ for (const section of sections) {
280
+ try {
281
+ const { widgets } = await fetchSectionsWidgets(page, section, opts);
282
+ out[section] = Array.isArray(widgets) ? widgets : [];
283
+ } catch (e) {
284
+ out[section] = [];
285
+ }
286
+ }
287
+ return out;
288
+ }
289
+
290
+ /**
291
+ * Fetch CMS page. GET /webstoreapi/pages/:handle
292
+ * @param {string} handle
293
+ * @param {Object} [opts]
294
+ * @returns {Promise<Object|null>} null on 404
295
+ */
296
+ async function fetchPage(handle, opts = {}) {
297
+ const { get } = createFetcher(opts);
298
+ try {
299
+ return await get(`/pages/${encodeURIComponent(handle)}`);
300
+ } catch (e) {
301
+ if (e.response?.status === 404) return null;
302
+ throw e;
303
+ }
304
+ }
305
+
306
+ module.exports = {
307
+ getWebstoreapiBaseUrl,
308
+ getShopfrontBaseUrl,
309
+ createFetcher,
310
+ fetchProducts,
311
+ fetchProductById,
312
+ fetchCategories,
313
+ fetchBrands,
314
+ fetchMenus,
315
+ fetchMenuById,
316
+ fetchCart,
317
+ fetchSectionsWidgets,
318
+ fetchWidgetsBySections,
319
+ fetchPage,
320
+ fetchStoreInfo,
321
+ DEFAULT_SECTIONS
322
+ };
@@ -305,6 +305,55 @@ class WidgetService {
305
305
  }
306
306
  }
307
307
 
308
+ /**
309
+ * Get widgets for a specific page and section.
310
+ * Matches production API: POST /pages/{pageId}/sections/{section}/widgets
311
+ * @param {string} pageId - Page ID (home, product, category, products, categories, brand, checkout)
312
+ * @param {string} section - Section name (header, hero, content, footer)
313
+ * @returns {Promise<Array>} Array of normalized widgets sorted by Position
314
+ */
315
+ async getPageSectionWidgets(pageId, section) {
316
+ try {
317
+ if (!this.apiClient) return [];
318
+
319
+ const response = await this.apiClient.post(`/pages/${pageId}/sections/${section}/widgets`, { status: 'active' });
320
+ const widgets = Array.isArray(response) ? response : (response.widgets || []);
321
+ return this.normalizeWidgetArray(widgets);
322
+ } catch (error) {
323
+ console.warn(`[WidgetService] Error fetching widgets for page=${pageId} section=${section}:`, error.message);
324
+ return [];
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Fetch all widgets for a page, organized by section.
330
+ * Mirrors the production fetchPageWidgets() helper.
331
+ * @param {string} pageId - Page ID
332
+ * @returns {Promise<Object>} Widgets organized by section { header:[], hero:[], content:[], footer:[] }
333
+ */
334
+ async fetchPageWidgets(pageId) {
335
+ const validPageIds = ['home', 'product', 'category', 'products', 'categories', 'brand', 'checkout'];
336
+ if (!pageId || !validPageIds.includes(pageId)) {
337
+ console.warn(`[WidgetService] Invalid pageId: ${pageId}, returning empty widgets`);
338
+ return { header: [], hero: [], content: [], footer: [] };
339
+ }
340
+
341
+ const sections = ['header', 'hero', 'content', 'footer'];
342
+ const results = await Promise.allSettled(
343
+ sections.map(section => this.getPageSectionWidgets(pageId, section))
344
+ );
345
+
346
+ const organized = {};
347
+ sections.forEach((section, index) => {
348
+ const result = results[index];
349
+ organized[section] = (result.status === 'fulfilled' && Array.isArray(result.value)) ? result.value : [];
350
+ });
351
+
352
+ const counts = sections.map(s => `${s}:${organized[s].length}`).join(', ');
353
+ console.log(`[WidgetService] fetchPageWidgets(${pageId}): ${counts}`);
354
+ return organized;
355
+ }
356
+
308
357
  /**
309
358
  * Check if widget template exists in theme
310
359
  * @param {string} widgetType - Widget type
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o2vend/theme-cli",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "O2VEND Theme Development CLI - Standalone tool for local theme development",
5
5
  "bin": {
6
6
  "o2vend": "./bin/o2vend"
@@ -22,28 +22,28 @@
22
22
  "unpublish-and-republish": "node scripts/unpublish-and-republish.js"
23
23
  },
24
24
  "dependencies": {
25
+ "archiver": "^6.0.1",
26
+ "axios": "^1.6.0",
27
+ "boxen": "^7.1.1",
28
+ "chalk": "^4.1.2",
29
+ "chokidar": "^3.5.3",
25
30
  "commander": "^11.0.0",
31
+ "dotenv": "^16.3.1",
26
32
  "express": "^4.18.2",
33
+ "fs-extra": "^11.1.1",
34
+ "glob": "^10.3.10",
35
+ "http-proxy-middleware": "^2.0.6",
36
+ "inquirer": "^9.2.0",
27
37
  "liquidjs": "^10.7.0",
28
- "axios": "^1.6.0",
38
+ "listr": "^0.14.3",
29
39
  "lodash": "^4.17.21",
30
40
  "moment": "^2.29.4",
41
+ "node-cache": "^5.1.2",
42
+ "open": "^8.4.2",
43
+ "ora": "^5.4.1",
31
44
  "slugify": "^1.6.6",
32
- "chokidar": "^3.5.3",
33
45
  "socket.io": "^4.7.0",
34
- "chalk": "^4.1.2",
35
- "inquirer": "^9.2.0",
36
- "ora": "^5.4.1",
37
- "boxen": "^7.1.1",
38
- "table": "^6.8.3",
39
- "listr": "^0.14.3",
40
- "dotenv": "^16.3.1",
41
- "archiver": "^6.0.1",
42
- "glob": "^10.3.10",
43
- "fs-extra": "^11.1.1",
44
- "node-cache": "^5.1.2",
45
- "http-proxy-middleware": "^2.0.6",
46
- "open": "^8.4.2"
46
+ "table": "^6.8.3"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=18.0.0"