@simonfestl/husky-cli 1.10.0 → 1.13.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/README.md +126 -0
- package/dist/commands/auth.js +176 -1
- package/dist/commands/biz/emove.d.ts +12 -0
- package/dist/commands/biz/emove.js +249 -0
- package/dist/commands/biz/skuterzone.d.ts +12 -0
- package/dist/commands/biz/skuterzone.js +248 -0
- package/dist/commands/biz/wattiz.d.ts +12 -0
- package/dist/commands/biz/wattiz.js +252 -0
- package/dist/commands/biz.js +7 -1
- package/dist/commands/config.d.ts +28 -0
- package/dist/commands/config.js +45 -2
- package/dist/commands/task.js +4 -2
- package/dist/index.js +2 -0
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +117 -0
- package/dist/lib/biz/emove-playwright.d.ts +22 -0
- package/dist/lib/biz/emove-playwright.js +289 -0
- package/dist/lib/biz/emove-types.d.ts +60 -0
- package/dist/lib/biz/emove-types.js +7 -0
- package/dist/lib/biz/skuterzone-playwright.d.ts +21 -0
- package/dist/lib/biz/skuterzone-playwright.js +271 -0
- package/dist/lib/biz/skuterzone-types.d.ts +60 -0
- package/dist/lib/biz/skuterzone-types.js +7 -0
- package/dist/lib/biz/sop.js +1 -14
- package/dist/lib/biz/wattiz-playwright.d.ts +22 -0
- package/dist/lib/biz/wattiz-playwright.js +380 -0
- package/dist/lib/biz/wattiz-types.d.ts +61 -0
- package/dist/lib/biz/wattiz-types.js +6 -0
- package/package.json +2 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wattiz Client with Playwright
|
|
3
|
+
*
|
|
4
|
+
* Uses real browser automation for PrestaShop platform
|
|
5
|
+
*/
|
|
6
|
+
import { chromium } from 'playwright';
|
|
7
|
+
import { getConfig } from '../../commands/config.js';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
export class WattizPlaywrightClient {
|
|
10
|
+
config;
|
|
11
|
+
browser;
|
|
12
|
+
page;
|
|
13
|
+
contextCreated = false;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
static fromConfig() {
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
const env = process.env.HUSKY_ENV || 'PROD';
|
|
20
|
+
const wattizConfig = {
|
|
21
|
+
username: process.env[`${env}_WATTIZ_USERNAME`] ||
|
|
22
|
+
process.env.WATTIZ_USERNAME ||
|
|
23
|
+
config.wattizUsername || '',
|
|
24
|
+
password: process.env[`${env}_WATTIZ_PASSWORD`] ||
|
|
25
|
+
process.env.WATTIZ_PASSWORD ||
|
|
26
|
+
config.wattizPassword || '',
|
|
27
|
+
baseUrl: process.env[`${env}_WATTIZ_BASE_URL`] ||
|
|
28
|
+
process.env.WATTIZ_BASE_URL ||
|
|
29
|
+
config.wattizBaseUrl ||
|
|
30
|
+
'https://www.wattiz.fr',
|
|
31
|
+
language: (process.env[`${env}_WATTIZ_LANGUAGE`] ||
|
|
32
|
+
process.env.WATTIZ_LANGUAGE ||
|
|
33
|
+
config.wattizLanguage ||
|
|
34
|
+
'gb'),
|
|
35
|
+
};
|
|
36
|
+
if (!wattizConfig.username || !wattizConfig.password) {
|
|
37
|
+
throw new Error('Missing Wattiz credentials. Configure with:\n' +
|
|
38
|
+
' husky config set wattiz-username <username>\n' +
|
|
39
|
+
' husky config set wattiz-password <password>');
|
|
40
|
+
}
|
|
41
|
+
return new WattizPlaywrightClient(wattizConfig);
|
|
42
|
+
}
|
|
43
|
+
async ensureBrowser() {
|
|
44
|
+
if (!this.browser || !this.page) {
|
|
45
|
+
this.browser = await chromium.launch({
|
|
46
|
+
headless: true,
|
|
47
|
+
args: [
|
|
48
|
+
'--no-sandbox',
|
|
49
|
+
'--disable-setuid-sandbox',
|
|
50
|
+
'--disable-dev-shm-usage',
|
|
51
|
+
'--disable-accelerated-2d-canvas',
|
|
52
|
+
'--disable-gpu'
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
if (!this.contextCreated) {
|
|
56
|
+
const context = await this.browser.newContext({
|
|
57
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
58
|
+
viewport: { width: 1920, height: 1080 },
|
|
59
|
+
locale: this.config.language === 'gb' ? 'en-GB' : 'fr-FR',
|
|
60
|
+
});
|
|
61
|
+
this.page = await context.newPage();
|
|
62
|
+
this.contextCreated = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return this.page;
|
|
66
|
+
}
|
|
67
|
+
async login() {
|
|
68
|
+
try {
|
|
69
|
+
const page = await this.ensureBrowser();
|
|
70
|
+
// Navigate to login page (PrestaShop - language-specific URLs)
|
|
71
|
+
// English uses /login, French uses /connexion
|
|
72
|
+
const loginPath = this.config.language === 'gb' ? 'login' : 'connexion';
|
|
73
|
+
await page.goto(`${this.config.baseUrl}/${this.config.language}/${loginPath}`, {
|
|
74
|
+
waitUntil: 'domcontentloaded',
|
|
75
|
+
timeout: 30000
|
|
76
|
+
});
|
|
77
|
+
await page.waitForLoadState('domcontentloaded').catch(() => { });
|
|
78
|
+
// PrestaShop can use different field patterns - try multiple selectors
|
|
79
|
+
const emailSelectors = [
|
|
80
|
+
'input[name="email"]',
|
|
81
|
+
'input[type="email"]',
|
|
82
|
+
'input#email',
|
|
83
|
+
'input[id*="email"]',
|
|
84
|
+
'input[name*="email"]'
|
|
85
|
+
];
|
|
86
|
+
const passwordSelectors = [
|
|
87
|
+
'input[name="password"]',
|
|
88
|
+
'input[type="password"]',
|
|
89
|
+
'input#password',
|
|
90
|
+
'input[id*="password"]'
|
|
91
|
+
];
|
|
92
|
+
// Find email input
|
|
93
|
+
let emailInput = null;
|
|
94
|
+
for (const selector of emailSelectors) {
|
|
95
|
+
const count = await page.locator(selector).count();
|
|
96
|
+
if (count > 0) {
|
|
97
|
+
emailInput = selector;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!emailInput) {
|
|
102
|
+
const html = await page.content();
|
|
103
|
+
await fs.promises.writeFile('/tmp/wattiz-login-debug.html', html);
|
|
104
|
+
throw new Error('Could not find email input field. Debug HTML saved to /tmp/wattiz-login-debug.html');
|
|
105
|
+
}
|
|
106
|
+
// Find password input
|
|
107
|
+
let passwordInput = null;
|
|
108
|
+
for (const selector of passwordSelectors) {
|
|
109
|
+
const count = await page.locator(selector).count();
|
|
110
|
+
if (count > 0) {
|
|
111
|
+
passwordInput = selector;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!passwordInput) {
|
|
116
|
+
throw new Error('Could not find password input field');
|
|
117
|
+
}
|
|
118
|
+
console.log(`Using selectors: email="${emailInput}", password="${passwordInput}"`);
|
|
119
|
+
// Fill in login form
|
|
120
|
+
await page.fill(emailInput, this.config.username);
|
|
121
|
+
await page.fill(passwordInput, this.config.password);
|
|
122
|
+
// Find and click submit button
|
|
123
|
+
const submitSelectors = [
|
|
124
|
+
'button[type="submit"]',
|
|
125
|
+
'button[name="submit"]',
|
|
126
|
+
'input[type="submit"]',
|
|
127
|
+
'button.btn-primary'
|
|
128
|
+
];
|
|
129
|
+
let submitClicked = false;
|
|
130
|
+
for (const selector of submitSelectors) {
|
|
131
|
+
const count = await page.locator(selector).count();
|
|
132
|
+
if (count > 0) {
|
|
133
|
+
await page.click(selector);
|
|
134
|
+
submitClicked = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!submitClicked) {
|
|
139
|
+
throw new Error('Could not find submit button');
|
|
140
|
+
}
|
|
141
|
+
// Wait for navigation
|
|
142
|
+
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => { });
|
|
143
|
+
// Check if logged in by looking for my-account page or logged-in indicators
|
|
144
|
+
const currentUrl = page.url();
|
|
145
|
+
console.log('After login, current URL:', currentUrl);
|
|
146
|
+
// Check for error messages first
|
|
147
|
+
const errorMsg = await page.locator('.alert-danger, .error-message, .ps-alert-error').textContent().catch(() => '');
|
|
148
|
+
if (errorMsg) {
|
|
149
|
+
console.log('Error message on page:', errorMsg.trim());
|
|
150
|
+
}
|
|
151
|
+
const isLoggedIn = currentUrl.includes('/my-account') ||
|
|
152
|
+
currentUrl.includes('/mon-compte') ||
|
|
153
|
+
await page.locator('.account-link').count() > 0 ||
|
|
154
|
+
await page.locator('[data-link-action="sign-out"]').count() > 0 ||
|
|
155
|
+
await page.locator('.logout, .sign-out').count() > 0 ||
|
|
156
|
+
await page.locator('a[href*="logout"]').count() > 0;
|
|
157
|
+
console.log('Is logged in:', isLoggedIn);
|
|
158
|
+
if (!isLoggedIn) {
|
|
159
|
+
// Save debug HTML
|
|
160
|
+
const debugHtml = await page.content();
|
|
161
|
+
await fs.promises.writeFile('/tmp/wattiz-after-login.html', debugHtml);
|
|
162
|
+
console.log('Debug HTML saved to /tmp/wattiz-after-login.html');
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
cookies: '',
|
|
166
|
+
error: `Login failed - check credentials. Current URL: ${currentUrl}`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// Extract cookies
|
|
170
|
+
const cookies = await page.context().cookies();
|
|
171
|
+
const cookieString = cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
cookies: cookieString
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
cookies: '',
|
|
181
|
+
error: `Login error: ${error.message}`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async listOrders() {
|
|
186
|
+
const page = await this.ensureBrowser();
|
|
187
|
+
// Navigate to orders page (PrestaShop order-history)
|
|
188
|
+
await page.goto(`${this.config.baseUrl}/${this.config.language}/order-history`);
|
|
189
|
+
await page.waitForLoadState('networkidle');
|
|
190
|
+
// Check if we need to login
|
|
191
|
+
const needsLogin = await page.url().includes('/login');
|
|
192
|
+
if (needsLogin) {
|
|
193
|
+
await this.login();
|
|
194
|
+
await page.goto(`${this.config.baseUrl}/${this.config.language}/order-history`);
|
|
195
|
+
await page.waitForLoadState('networkidle');
|
|
196
|
+
}
|
|
197
|
+
const orders = [];
|
|
198
|
+
// Find all order rows (PrestaShop structure)
|
|
199
|
+
const orderRows = page.locator('table tbody tr, .order-line');
|
|
200
|
+
const count = await orderRows.count();
|
|
201
|
+
for (let i = 0; i < count; i++) {
|
|
202
|
+
const row = orderRows.nth(i);
|
|
203
|
+
// PrestaShop order structure
|
|
204
|
+
const orderLinkEl = row.locator('a[href*="order-detail"]').first();
|
|
205
|
+
const orderLink = await orderLinkEl.getAttribute('href').catch(() => '');
|
|
206
|
+
// Extract order ID from PrestaShop controller URL
|
|
207
|
+
const orderIdMatch = orderLink?.match(/id_order=(\d+)/);
|
|
208
|
+
const orderId = orderIdMatch ? orderIdMatch[1] : '';
|
|
209
|
+
const orderNumber = await orderLinkEl.textContent().catch(() => '') || orderId;
|
|
210
|
+
const date = (await row.locator('.order-date, td:nth-child(2)').textContent().catch(() => '')) || '';
|
|
211
|
+
const status = (await row.locator('.order-status, td:nth-child(4), .label').textContent().catch(() => '')) || '';
|
|
212
|
+
const total = (await row.locator('.order-total, td:nth-child(3)').textContent().catch(() => '')) || '';
|
|
213
|
+
if (orderId) {
|
|
214
|
+
orders.push({
|
|
215
|
+
id: orderId,
|
|
216
|
+
orderNumber: orderNumber.trim(),
|
|
217
|
+
date: date.trim(),
|
|
218
|
+
status: status.trim(),
|
|
219
|
+
total: total.trim(),
|
|
220
|
+
itemCount: 0,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return orders;
|
|
225
|
+
}
|
|
226
|
+
async getOrder(orderId) {
|
|
227
|
+
const page = await this.ensureBrowser();
|
|
228
|
+
// PrestaShop controller-based URL
|
|
229
|
+
await page.goto(`${this.config.baseUrl}/${this.config.language}/index.php?controller=order-detail&id_order=${orderId}`);
|
|
230
|
+
await page.waitForLoadState('networkidle');
|
|
231
|
+
// Check if we need to login
|
|
232
|
+
const needsLogin = await page.url().includes('/login');
|
|
233
|
+
if (needsLogin) {
|
|
234
|
+
await this.login();
|
|
235
|
+
await page.goto(`${this.config.baseUrl}/${this.config.language}/index.php?controller=order-detail&id_order=${orderId}`);
|
|
236
|
+
await page.waitForLoadState('networkidle');
|
|
237
|
+
}
|
|
238
|
+
// Extract order information
|
|
239
|
+
const orderNumber = (await page.locator('.order-reference, h3').first().textContent().catch(() => orderId)) || orderId;
|
|
240
|
+
const orderDate = (await page.locator('.order-date, .date').first().textContent().catch(() => '')) || '';
|
|
241
|
+
const orderStatus = (await page.locator('.order-status, .label').first().textContent().catch(() => '')) || '';
|
|
242
|
+
// Extract customer info
|
|
243
|
+
let customerName = '';
|
|
244
|
+
let customerAddress = '';
|
|
245
|
+
let customerEmail = '';
|
|
246
|
+
let customerPhone = '';
|
|
247
|
+
try {
|
|
248
|
+
const addressBlock = page.locator('.address, .delivery-address').first();
|
|
249
|
+
const addressText = await addressBlock.textContent() || '';
|
|
250
|
+
const lines = addressText.split('\n').map(l => l.trim()).filter(l => l);
|
|
251
|
+
if (lines.length > 0)
|
|
252
|
+
customerName = lines[0];
|
|
253
|
+
if (lines.length > 1)
|
|
254
|
+
customerAddress = lines.slice(1).filter(l => !l.includes('@') && !l.startsWith('+')).join(', ');
|
|
255
|
+
// Try to find email and phone
|
|
256
|
+
customerEmail = await page.locator('[href^="mailto:"]').first().textContent().catch(() => '') || '';
|
|
257
|
+
customerPhone = await page.locator('[href^="tel:"]').first().textContent().catch(() => '') || '';
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
// Customer details not available
|
|
261
|
+
}
|
|
262
|
+
// Extract line items
|
|
263
|
+
const items = [];
|
|
264
|
+
const itemRows = page.locator('.order-products table tbody tr, .product-line-row');
|
|
265
|
+
const itemCount = await itemRows.count();
|
|
266
|
+
for (let i = 0; i < itemCount; i++) {
|
|
267
|
+
const itemRow = itemRows.nth(i);
|
|
268
|
+
const name = await itemRow.locator('.product-name, td:first-child').textContent() || '';
|
|
269
|
+
const qty = await itemRow.locator('.qty, td:nth-child(2)').textContent() || '1';
|
|
270
|
+
const total = await itemRow.locator('.price, td:last-child').textContent() || '';
|
|
271
|
+
items.push({
|
|
272
|
+
sku: '',
|
|
273
|
+
name: name.trim(),
|
|
274
|
+
quantity: parseInt(qty.replace(/\D/g, ''), 10) || 1,
|
|
275
|
+
price: '',
|
|
276
|
+
total: total.trim(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// Look for invoice link
|
|
280
|
+
const invoiceLink = await page.locator('a[href*="invoice"], a.btn-primary[href*="pdf"]').first().getAttribute('href').catch(() => '');
|
|
281
|
+
// Extract total
|
|
282
|
+
const totalText = await page.locator('.order-total, .total-value').last().textContent().catch(() => '');
|
|
283
|
+
// Extract tracking number if available
|
|
284
|
+
const trackingNumber = await page.locator('.tracking-number, [href*="track"]').first().textContent().catch(() => '');
|
|
285
|
+
return {
|
|
286
|
+
id: orderId,
|
|
287
|
+
orderNumber: orderNumber?.trim() || orderId,
|
|
288
|
+
date: orderDate?.trim() || '',
|
|
289
|
+
status: orderStatus?.trim() || '',
|
|
290
|
+
total: totalText?.trim() || '',
|
|
291
|
+
itemCount: items.length,
|
|
292
|
+
invoiceUrl: invoiceLink || undefined,
|
|
293
|
+
trackingNumber: trackingNumber?.trim() || undefined,
|
|
294
|
+
customer: {
|
|
295
|
+
name: customerName.trim(),
|
|
296
|
+
address: customerAddress.trim(),
|
|
297
|
+
city: '',
|
|
298
|
+
postcode: '',
|
|
299
|
+
email: customerEmail.trim(),
|
|
300
|
+
phone: customerPhone.trim(),
|
|
301
|
+
},
|
|
302
|
+
items,
|
|
303
|
+
subtotal: '',
|
|
304
|
+
shipping: '',
|
|
305
|
+
tax: '',
|
|
306
|
+
paymentMethod: '',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async downloadInvoice(orderId, savePath) {
|
|
310
|
+
try {
|
|
311
|
+
const order = await this.getOrder(orderId);
|
|
312
|
+
if (!order.invoiceUrl) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const page = await this.ensureBrowser();
|
|
316
|
+
// Construct full URL
|
|
317
|
+
const invoiceUrl = order.invoiceUrl.startsWith('http')
|
|
318
|
+
? order.invoiceUrl
|
|
319
|
+
: `${this.config.baseUrl}${order.invoiceUrl}`;
|
|
320
|
+
// Set up download listener before navigating
|
|
321
|
+
const downloadPromise = page.waitForEvent('download', { timeout: 30000 });
|
|
322
|
+
// Navigate to invoice URL (this triggers the download)
|
|
323
|
+
await page.goto(invoiceUrl, { waitUntil: 'commit' }).catch(() => {
|
|
324
|
+
// Ignore navigation error since download starts immediately
|
|
325
|
+
});
|
|
326
|
+
// Wait for the download to start
|
|
327
|
+
const download = await downloadPromise;
|
|
328
|
+
// Save the downloaded file
|
|
329
|
+
await download.saveAs(savePath);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error('Invoice download error:', error);
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async searchProducts(query) {
|
|
338
|
+
const page = await this.ensureBrowser();
|
|
339
|
+
const searchUrl = `${this.config.baseUrl}/${this.config.language}/search?s=${encodeURIComponent(query)}`;
|
|
340
|
+
await page.goto(searchUrl);
|
|
341
|
+
await page.waitForLoadState('networkidle');
|
|
342
|
+
// Check if we need to login to see prices (B2B feature)
|
|
343
|
+
const needsLogin = await page.url().includes('/login');
|
|
344
|
+
if (needsLogin) {
|
|
345
|
+
await this.login();
|
|
346
|
+
await page.goto(searchUrl);
|
|
347
|
+
await page.waitForLoadState('networkidle');
|
|
348
|
+
}
|
|
349
|
+
const products = [];
|
|
350
|
+
// Find all product items (PrestaShop structure)
|
|
351
|
+
const productItems = page.locator('.product-miniature, .js-product-miniature, article.product');
|
|
352
|
+
const count = await productItems.count();
|
|
353
|
+
for (let i = 0; i < count; i++) {
|
|
354
|
+
const item = productItems.nth(i);
|
|
355
|
+
const link = item.locator('a.product-thumbnail, h3 a').first();
|
|
356
|
+
const url = await link.getAttribute('href') || '';
|
|
357
|
+
const name = await item.locator('.product-title, h3 a, h2 a').first().textContent() || '';
|
|
358
|
+
const price = await item.locator('.price, .product-price-and-shipping').first().textContent().catch(() => undefined);
|
|
359
|
+
const img = await item.locator('img').first().getAttribute('src');
|
|
360
|
+
if (name && url) {
|
|
361
|
+
products.push({
|
|
362
|
+
id: '',
|
|
363
|
+
name: name.trim(),
|
|
364
|
+
url,
|
|
365
|
+
price: price?.trim(),
|
|
366
|
+
imageUrl: img || undefined,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return products;
|
|
371
|
+
}
|
|
372
|
+
async close() {
|
|
373
|
+
if (this.browser) {
|
|
374
|
+
await this.browser.close();
|
|
375
|
+
this.browser = undefined;
|
|
376
|
+
this.page = undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
export default WattizPlaywrightClient;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wattiz Types
|
|
3
|
+
*
|
|
4
|
+
* TypeScript interfaces for wattiz.fr (B2B e-mobility parts supplier - PrestaShop)
|
|
5
|
+
*/
|
|
6
|
+
export interface WattizConfig {
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
language: 'gb' | 'fr' | 'de' | 'es';
|
|
11
|
+
}
|
|
12
|
+
export interface WattizOrder {
|
|
13
|
+
id: string;
|
|
14
|
+
orderNumber: string;
|
|
15
|
+
date: string;
|
|
16
|
+
status: string;
|
|
17
|
+
total: string;
|
|
18
|
+
itemCount: number;
|
|
19
|
+
invoiceUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WattizOrderDetails extends WattizOrder {
|
|
22
|
+
customer: {
|
|
23
|
+
name: string;
|
|
24
|
+
company?: string;
|
|
25
|
+
address: string;
|
|
26
|
+
city: string;
|
|
27
|
+
postcode: string;
|
|
28
|
+
email: string;
|
|
29
|
+
phone?: string;
|
|
30
|
+
};
|
|
31
|
+
items: WattizLineItem[];
|
|
32
|
+
subtotal: string;
|
|
33
|
+
shipping: string;
|
|
34
|
+
tax: string;
|
|
35
|
+
paymentMethod: string;
|
|
36
|
+
shippingMethod?: string;
|
|
37
|
+
trackingNumber?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface WattizLineItem {
|
|
40
|
+
sku: string;
|
|
41
|
+
name: string;
|
|
42
|
+
quantity: number;
|
|
43
|
+
price: string;
|
|
44
|
+
total: string;
|
|
45
|
+
}
|
|
46
|
+
export interface WattizProduct {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
sku?: string;
|
|
50
|
+
ean?: string;
|
|
51
|
+
price?: string;
|
|
52
|
+
url: string;
|
|
53
|
+
imageUrl?: string;
|
|
54
|
+
stockStatus?: string;
|
|
55
|
+
category?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface WattizLoginResult {
|
|
58
|
+
success: boolean;
|
|
59
|
+
cookies: string;
|
|
60
|
+
error?: string;
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonfestl/husky-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@inquirer/prompts": "^8.1.0",
|
|
26
26
|
"commander": "^12.1.0",
|
|
27
27
|
"firebase-admin": "^13.6.0",
|
|
28
|
+
"playwright": "^1.57.0",
|
|
28
29
|
"sharp": "^0.34.5",
|
|
29
30
|
"youtube-transcript": "^1.2.1",
|
|
30
31
|
"zod": "^4.3.5"
|