@cloudcommerce/cli 2.37.3 → 2.38.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/lib/cli.js +6 -2
- package/lib/ext/import-feed.js +236 -0
- package/lib/login.js +6 -4
- package/package.json +3 -2
package/lib/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import login from './login.js';
|
|
|
9
9
|
import build, { prepareCodebases } from './build.js';
|
|
10
10
|
import { siginGcloudAndSetIAM, createServiceAccountKey } from './setup-gcloud.js';
|
|
11
11
|
import createGhSecrets from './setup-gh.js';
|
|
12
|
+
import importFeed from './ext/import-feed.js';
|
|
12
13
|
|
|
13
14
|
if (!process.env.FIREBASE_PROJECT_ID && !process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
14
15
|
const pwd = process.cwd();
|
|
@@ -151,7 +152,7 @@ ECOM_STORE_ID=${storeId}
|
|
|
151
152
|
}
|
|
152
153
|
if (hasCreatedAllSecrets) {
|
|
153
154
|
return echo`
|
|
154
|
-
|
|
155
|
+
****
|
|
155
156
|
|
|
156
157
|
CloudCommerce setup completed successfully.
|
|
157
158
|
Your store repository on GitHub is ready, the first deploy will automatically start with GH Actions.
|
|
@@ -160,7 +161,7 @@ Your store repository on GitHub is ready, the first deploy will automatically st
|
|
|
160
161
|
`;
|
|
161
162
|
}
|
|
162
163
|
return echo`
|
|
163
|
-
|
|
164
|
+
****
|
|
164
165
|
|
|
165
166
|
Finish by saving the following secrets to your GitHub repository:
|
|
166
167
|
|
|
@@ -185,6 +186,9 @@ Finish by saving the following secrets to your GitHub repository:
|
|
|
185
186
|
const port = typeof argv.port === 'string' || typeof argv.port === 'number' ? argv.port : '';
|
|
186
187
|
return $`npm --prefix "${prefix}" run dev -- --host ${host} --port ${port}`;
|
|
187
188
|
}
|
|
189
|
+
if (argv._.includes('import')) {
|
|
190
|
+
return importFeed();
|
|
191
|
+
}
|
|
188
192
|
return $`echo 'Hello from @cloudcommerce/cli'`;
|
|
189
193
|
};
|
|
190
194
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
argv, fs, echo, sleep,
|
|
3
|
+
} from 'zx';
|
|
4
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
5
|
+
import api from '@cloudcommerce/api';
|
|
6
|
+
|
|
7
|
+
const clearAccents = (str) => {
|
|
8
|
+
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
9
|
+
};
|
|
10
|
+
const slugify = (name) => {
|
|
11
|
+
return clearAccents(name).toLocaleLowerCase()
|
|
12
|
+
.replace(/[\s\n/]/g, '-')
|
|
13
|
+
.replace(/[^a-z0-9_-]/ig, '');
|
|
14
|
+
};
|
|
15
|
+
const importFeed = async () => {
|
|
16
|
+
const feedFilepath = typeof argv.feed === 'string' ? argv.feed : '';
|
|
17
|
+
if (!feedFilepath) {
|
|
18
|
+
await echo`You must specify XML file to import with --feed= argument`;
|
|
19
|
+
return process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
const parser = new XMLParser({
|
|
22
|
+
ignoreAttributes: false,
|
|
23
|
+
attributeNamePrefix: '',
|
|
24
|
+
});
|
|
25
|
+
const json = parser.parse(fs.readFileSync(argv.feed, 'utf8'));
|
|
26
|
+
const items = json.rss?.channel?.item?.filter?.((item) => {
|
|
27
|
+
return item?.['g:id'] && item['g:title'];
|
|
28
|
+
});
|
|
29
|
+
if (!items?.[0] || typeof items?.[0] !== 'object') {
|
|
30
|
+
await echo`The XML file does not appear to be a valid RSS 2.0 product feed`;
|
|
31
|
+
return process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const categoryNames = [];
|
|
34
|
+
const brandNames = [];
|
|
35
|
+
items.forEach(async (item) => {
|
|
36
|
+
const categoryName = item['g:product_type']?.split('>').pop()?.trim();
|
|
37
|
+
if (categoryName && !categoryNames.includes(categoryName)) {
|
|
38
|
+
categoryNames.push(categoryName);
|
|
39
|
+
}
|
|
40
|
+
const brandName = item['g:brand']?.trim();
|
|
41
|
+
if (brandName && !brandNames.includes(brandName)) {
|
|
42
|
+
brandNames.push(brandName);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const categories = [];
|
|
46
|
+
/* eslint-disable no-await-in-loop */
|
|
47
|
+
for (let i = 0; i < categoryNames.length; i++) {
|
|
48
|
+
const categoryName = categoryNames[i];
|
|
49
|
+
const { data: { result } } = await api.get('categories', {
|
|
50
|
+
params: { name: categoryName },
|
|
51
|
+
fields: [
|
|
52
|
+
'name',
|
|
53
|
+
'i18n',
|
|
54
|
+
'slug',
|
|
55
|
+
'parent',
|
|
56
|
+
'ml_category_id',
|
|
57
|
+
'google_product_category_id',
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
if (result[0]) {
|
|
61
|
+
categories.push(result[0]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const category = {
|
|
65
|
+
name: categoryName,
|
|
66
|
+
slug: slugify(categoryName),
|
|
67
|
+
};
|
|
68
|
+
const { data: { _id } } = await api.post('categories', category);
|
|
69
|
+
categories.push({ ...category, _id });
|
|
70
|
+
}
|
|
71
|
+
const brands = [];
|
|
72
|
+
for (let i = 0; i < brandNames.length; i++) {
|
|
73
|
+
const brandName = brandNames[i];
|
|
74
|
+
const { data: { result } } = await api.get('brands', {
|
|
75
|
+
params: { name: brandName },
|
|
76
|
+
fields: [
|
|
77
|
+
'name',
|
|
78
|
+
'i18n',
|
|
79
|
+
'slug',
|
|
80
|
+
'logo',
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
if (result[0]) {
|
|
84
|
+
brands.push(result[0]);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const brand = {
|
|
88
|
+
name: brandName,
|
|
89
|
+
slug: slugify(brandName),
|
|
90
|
+
};
|
|
91
|
+
const { data: { _id } } = await api.post('brands', brand);
|
|
92
|
+
brands.push({ ...brand, _id });
|
|
93
|
+
}
|
|
94
|
+
await echo`\nCategories:`;
|
|
95
|
+
for (let i = 0; i < categories.length; i++) {
|
|
96
|
+
await echo`${categories[i].name} (${categories[i].slug})`;
|
|
97
|
+
}
|
|
98
|
+
await echo`\nBrands:`;
|
|
99
|
+
for (let i = 0; i < brands.length; i++) {
|
|
100
|
+
await echo`${brands[i].name} (${brands[i].slug})`;
|
|
101
|
+
}
|
|
102
|
+
const productItems = items.filter((item) => !item['g:item_group_id']);
|
|
103
|
+
items.forEach((item) => {
|
|
104
|
+
const parentSku = item['g:item_group_id'];
|
|
105
|
+
if (!parentSku || productItems.find((i) => i['g:id'] === parentSku)) return;
|
|
106
|
+
productItems.push({
|
|
107
|
+
...item,
|
|
108
|
+
'g:id': parentSku,
|
|
109
|
+
'g:title': item['g:title']?.replace(/\s[A-Z]{1,3}$/, ''),
|
|
110
|
+
'g:description': item['g:title'] === item['g:description']
|
|
111
|
+
? undefined
|
|
112
|
+
: item['g:description'],
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const productItemGroups = [];
|
|
116
|
+
for (let i = 0; i < productItems.length; i += 20) {
|
|
117
|
+
productItemGroups.push(productItems.slice(i, i + 20));
|
|
118
|
+
}
|
|
119
|
+
for (let i = 0; i < productItemGroups.length; i++) {
|
|
120
|
+
const { data: { result } } = await api.get('products', {
|
|
121
|
+
params: {
|
|
122
|
+
sku: productItemGroups[i].map(({ 'g:id': sku }) => sku),
|
|
123
|
+
},
|
|
124
|
+
fields: ['sku'],
|
|
125
|
+
});
|
|
126
|
+
for (let ii = 0; ii < productItems.length; ii++) {
|
|
127
|
+
if (result.find(({ sku }) => sku === productItems[ii]['g:id'])) {
|
|
128
|
+
productItems.splice(ii, 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const setStockAndPrices = (productOrVatiation, item) => {
|
|
133
|
+
productOrVatiation.quantity = item['g:availability'] === 'in stock' ? 100 : 0;
|
|
134
|
+
const price = parseFloat(item['g:price'] || item['g:sale_price'] || '');
|
|
135
|
+
if (price) {
|
|
136
|
+
const salePrice = parseFloat(item['g:sale_price'] || '');
|
|
137
|
+
productOrVatiation.price = salePrice || price;
|
|
138
|
+
if (salePrice && salePrice < price) productOrVatiation.base_price = price;
|
|
139
|
+
}
|
|
140
|
+
if (item['g:shipping_weight']) {
|
|
141
|
+
const [weight, unit] = item['g:shipping_weight'].split(' ');
|
|
142
|
+
if (parseFloat(weight)) {
|
|
143
|
+
productOrVatiation.weight = {
|
|
144
|
+
value: parseFloat(weight),
|
|
145
|
+
unit: unit === 'g' || unit === 'kg' ? unit : 'kg',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const setSpecifications = (productOrVatiation, item, isProductItem = false) => {
|
|
151
|
+
const specifications = {};
|
|
152
|
+
if (item['g:color']) {
|
|
153
|
+
if (/[A-Z]{1,3}/.test(item['g:color'])) {
|
|
154
|
+
item['g:size'] = item['g:color'];
|
|
155
|
+
delete item['g:color'];
|
|
156
|
+
} else {
|
|
157
|
+
specifications.colors = [{ text: item['g:color'] }];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
[
|
|
161
|
+
'material',
|
|
162
|
+
'pattern',
|
|
163
|
+
'size',
|
|
164
|
+
'size_system',
|
|
165
|
+
'gender',
|
|
166
|
+
].forEach((spec) => {
|
|
167
|
+
if (!isProductItem && (spec === 'size_system' || spec === 'gender')) return;
|
|
168
|
+
const text = item[`g:${spec}`];
|
|
169
|
+
if (text) {
|
|
170
|
+
specifications[spec] = [{ text }];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
if (Object.keys(specifications).length) {
|
|
174
|
+
productOrVatiation.specifications = specifications;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
await echo`\n`;
|
|
178
|
+
for (let i = 0; i < productItems.length; i++) {
|
|
179
|
+
const item = productItems[i];
|
|
180
|
+
const { 'g:id': sku, 'g:title': name } = item;
|
|
181
|
+
if (!sku || !name) continue;
|
|
182
|
+
await echo`SKU: ${sku}`;
|
|
183
|
+
const product = {
|
|
184
|
+
sku,
|
|
185
|
+
name,
|
|
186
|
+
slug: item['g:link']
|
|
187
|
+
? new URL(item['g:link']).pathname.substring(1)
|
|
188
|
+
: slugify(name),
|
|
189
|
+
condition: item['g:condition'],
|
|
190
|
+
};
|
|
191
|
+
const description = item['g:description']?.trim();
|
|
192
|
+
if (description && description !== name) {
|
|
193
|
+
product.body_html = description;
|
|
194
|
+
}
|
|
195
|
+
const categoryName = item['g:product_type']?.split('>').pop()?.trim();
|
|
196
|
+
const category = categoryName && categories.find((c) => c.name === categoryName);
|
|
197
|
+
if (category) {
|
|
198
|
+
product.categories = [category];
|
|
199
|
+
}
|
|
200
|
+
const brandName = item['g:brand']?.trim();
|
|
201
|
+
const brand = brandName && brands.find((b) => b.name === brandName);
|
|
202
|
+
if (brand) {
|
|
203
|
+
product.brands = [brand];
|
|
204
|
+
}
|
|
205
|
+
setStockAndPrices(product, item);
|
|
206
|
+
const variationItems = items.filter((_item) => {
|
|
207
|
+
return _item['g:item_group_id'] === sku;
|
|
208
|
+
});
|
|
209
|
+
if (variationItems.length) {
|
|
210
|
+
let totalQuantity = 0;
|
|
211
|
+
product.variations = [];
|
|
212
|
+
variationItems.forEach((variationItem) => {
|
|
213
|
+
const variation = {
|
|
214
|
+
sku: variationItem['g:id'],
|
|
215
|
+
name: variationItem['g:title'],
|
|
216
|
+
specifications: {},
|
|
217
|
+
};
|
|
218
|
+
['g:shipping_weight', 'g:price', 'g:sale_price'].forEach((field) => {
|
|
219
|
+
if (item[field] === variationItem[field]) delete variationItem[field];
|
|
220
|
+
});
|
|
221
|
+
setStockAndPrices(variation, variationItem);
|
|
222
|
+
totalQuantity += variation.quantity || 0;
|
|
223
|
+
setSpecifications(variation, variationItem);
|
|
224
|
+
product.variations.push(variation);
|
|
225
|
+
});
|
|
226
|
+
product.quantity = totalQuantity;
|
|
227
|
+
} else {
|
|
228
|
+
setSpecifications(product, item, true);
|
|
229
|
+
}
|
|
230
|
+
await sleep(500);
|
|
231
|
+
await echo`${JSON.stringify(product, null, 2)}`;
|
|
232
|
+
}
|
|
233
|
+
return echo``;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default importFeed;
|
package/lib/login.js
CHANGED
|
@@ -4,7 +4,7 @@ import md5 from 'md5';
|
|
|
4
4
|
import api from '@cloudcommerce/api';
|
|
5
5
|
import createAuth from './create-auth.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const login = async () => {
|
|
8
8
|
await echo`-- Login with your E-Com Plus store admin account.
|
|
9
9
|
(i) same credentials used to enter the dashboard (https://ecomplus.app/)
|
|
10
10
|
`;
|
|
@@ -28,11 +28,13 @@ export default async () => {
|
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
});
|
|
31
|
-
const { data:
|
|
31
|
+
const { data: credentials } = await api.post('login', {
|
|
32
32
|
username,
|
|
33
33
|
pass_md5_hash: passMd5,
|
|
34
34
|
});
|
|
35
|
-
const storeId =
|
|
36
|
-
const { data: { access_token: accessToken } } = await api.post('authenticate',
|
|
35
|
+
const storeId = credentials.store_ids[0];
|
|
36
|
+
const { data: { access_token: accessToken } } = await api.post('authenticate', credentials);
|
|
37
37
|
return createAuth(storeId, accessToken);
|
|
38
38
|
};
|
|
39
|
+
|
|
40
|
+
export default login;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcommerce/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.38.0",
|
|
5
5
|
"description": "e-com.plus Cloud Commerce CLI tools",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cloudcommerce": "./bin/run.mjs"
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@fastify/deepmerge": "^2.0.2",
|
|
34
34
|
"dotenv": "^16.4.7",
|
|
35
|
+
"fast-xml-parser": "^5.0.8",
|
|
35
36
|
"libsodium-wrappers": "^0.7.15",
|
|
36
37
|
"md5": "^2.3.0",
|
|
37
38
|
"typescript": "~5.8.2",
|
|
38
39
|
"zx": "^8.3.2",
|
|
39
|
-
"@cloudcommerce/api": "2.
|
|
40
|
+
"@cloudcommerce/api": "2.38.0"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "bash ../../scripts/build-lib.sh"
|