mage-remote-run 0.17.0 → 0.19.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.
@@ -226,14 +226,16 @@ Examples:
226
226
  const answers = await inquirer.prompt([
227
227
  { name: 'company_name', message: 'Company Name:', default: current.company_name },
228
228
  { name: 'company_email', message: 'Company Email:', default: current.company_email },
229
- { name: 'sales_representative_id', message: 'Sales Rep ID:', default: current.sales_representative_id },
230
- { name: 'customer_group_id', message: 'Customer Group ID:', default: current.customer_group_id }
229
+ { name: 'sales_representative_id', message: 'Sales Rep ID:', default: String(current.sales_representative_id) },
230
+ { name: 'customer_group_id', message: 'Customer Group ID:', default: String(current.customer_group_id) }
231
231
  ]);
232
232
 
233
233
  // Merge
234
234
  const payload = {
235
- ...current,
236
- ...answers
235
+ company: {
236
+ ...current,
237
+ ...answers
238
+ }
237
239
  };
238
240
 
239
241
  await client.put(`V1/company/${companyId}`, payload);
@@ -18,12 +18,14 @@ export function registerConsoleCommand(program) {
18
18
  console.log(chalk.gray('Debug mode enabled'));
19
19
  }
20
20
 
21
- console.log(chalk.bold.blue('Mage Remote Run Interactive Console'));
22
- console.log(chalk.gray('Type your commands directly or write JS code.'));
23
- console.log(chalk.gray('Global variables available: client (async factory), config, chalk'));
24
- console.log(chalk.gray('Example JS: await (await client()).get("V1/store/websites")'));
25
- console.log(chalk.gray('Type "list" to see available commands.'));
26
- console.log(chalk.gray('Type .exit to quit.\n'));
21
+ if (process.env.NODE_ENV !== 'test') {
22
+ console.log(chalk.bold.blue('Mage Remote Run Interactive Console'));
23
+ console.log(chalk.gray('Type your commands directly or write JS code.'));
24
+ console.log(chalk.gray('Global variables available: client (async factory), config, chalk'));
25
+ console.log(chalk.gray('Example JS: await (await client()).get("V1/store/websites")'));
26
+ console.log(chalk.gray('Type "list" to see available commands.'));
27
+ console.log(chalk.gray('Type .exit to quit.\n'));
28
+ }
27
29
 
28
30
  // State for the REPL
29
31
  let localProgram;
@@ -1,24 +1,18 @@
1
1
  import { createClient } from '../api/factory.js';
2
2
  import { printTable, handleError } from '../utils.js';
3
3
  import chalk from 'chalk';
4
+ import { input, select, confirm } from '@inquirer/prompts';
5
+ import search from '@inquirer/search';
4
6
 
5
7
  export function registerWebhooksCommands(program) {
6
- const webhooks = program.command('webhook').description('Manage webhooks');
7
-
8
+ const webhooks = program.command('webhook').description('Manage webhooks (SaaS)');
8
9
 
9
10
  //-------------------------------------------------------
10
11
  // "webhook list" Command
11
12
  //-------------------------------------------------------
12
13
  webhooks.command('list')
13
14
  .description('List available webhooks')
14
- .option('-p, --page <number>', 'Page number', '1')
15
- .option('-s, --size <number>', 'Page size', '20')
16
15
  .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
17
- .addHelpText('after', `
18
- Examples:
19
- $ mage-remote-run webhook list
20
- $ mage-remote-run webhook list --format json
21
- `)
22
16
  .action(async (options) => {
23
17
  try {
24
18
  const client = await createClient();
@@ -26,12 +20,8 @@ Examples:
26
20
  if (options.format === 'json') headers['Accept'] = 'application/json';
27
21
  else if (options.format === 'xml') headers['Accept'] = 'application/xml';
28
22
 
29
- const params = {
30
- 'searchCriteria[currentPage]': options.page,
31
- 'searchCriteria[pageSize]': options.size
32
- };
33
-
34
- const data = await client.get('V1/webhooks/list', params, { headers });
23
+ // SaaS API returns an array directly
24
+ const data = await client.get('V1/webhooks/list', {}, { headers });
35
25
 
36
26
  if (options.format === 'json') {
37
27
  console.log(JSON.stringify(data, null, 2));
@@ -42,16 +32,465 @@ Examples:
42
32
  return;
43
33
  }
44
34
 
45
- const rows = (data.items || []).map(w => [
46
- w.hook_id,
47
- w.name,
48
- w.status,
49
- w.store_ids ? w.store_ids.join(',') : '-',
35
+ const rows = (Array.isArray(data) ? data : []).map(w => [
36
+ w.hook_name,
37
+ w.webhook_type,
38
+ w.method,
39
+ w.priority !== undefined ? w.priority : '-',
50
40
  w.url
51
41
  ]);
52
42
 
53
- console.log(chalk.bold(`Total: ${data.total_count}, Page: ${options.page}, Size: ${options.size}`));
54
- printTable(['ID', 'Name', 'Status', 'Store IDs', 'URL'], rows);
43
+ console.log(chalk.bold(`Total: ${rows.length}`));
44
+ printTable(['Name', 'Type', 'Method', 'Priority', 'URL'], rows);
55
45
  } catch (e) { handleError(e); }
56
46
  });
47
+
48
+ //-------------------------------------------------------
49
+ // "webhook supported-list" Command
50
+ //-------------------------------------------------------
51
+ webhooks.command('supported-list')
52
+ .description('List supported webhook types')
53
+ .option('--format <format>', 'Output format (table/json)', 'table')
54
+ .action(async (options) => {
55
+ try {
56
+ const client = await createClient();
57
+
58
+ const headers = {};
59
+ if (options.format === 'json') headers['Accept'] = 'application/json';
60
+
61
+ const data = await client.get('V1/webhooks/supportedList', {}, { headers });
62
+
63
+ if (options.format === 'json') {
64
+ console.log(JSON.stringify(data, null, 2));
65
+ return;
66
+ }
67
+
68
+ // Extract webhook names from the response
69
+ const rows = (Array.isArray(data) ? data : []).map(w => [w.name]);
70
+
71
+ console.log(chalk.bold(`Total: ${rows.length}`));
72
+ printTable(['Webhook Method'], rows);
73
+ } catch (e) { handleError(e); }
74
+ });
75
+
76
+ //-------------------------------------------------------
77
+ // "webhook show" Command
78
+ //-------------------------------------------------------
79
+ webhooks.command('show')
80
+ .description('Get webhook details')
81
+ .argument('[name]', 'Webhook Name')
82
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
83
+ .action(async (name, options) => {
84
+ try {
85
+ const client = await createClient();
86
+
87
+ // Always fetch list first as there is no direct "get by name" endpoint in swagger
88
+ const listData = await client.get('V1/webhooks/list');
89
+ const items = Array.isArray(listData) ? listData : [];
90
+
91
+ if (!name) {
92
+ if (items.length === 0) {
93
+ console.log(chalk.yellow('No webhooks found.'));
94
+ return;
95
+ }
96
+ name = await select({
97
+ message: 'Select Webhook:',
98
+ choices: items.map(w => ({
99
+ name: `${w.hook_name} (${w.webhook_type})`,
100
+ value: w.hook_name
101
+ }))
102
+ });
103
+ }
104
+
105
+ const webhook = items.find(w => w.hook_name === name);
106
+
107
+ if (!webhook) {
108
+ throw new Error(`Webhook "${name}" not found.`);
109
+ }
110
+
111
+ if (options.format === 'json') {
112
+ console.log(JSON.stringify(webhook, null, 2));
113
+ return;
114
+ }
115
+ if (options.format === 'xml') {
116
+ // Primitive XML conversion if needed, but JSON is standard
117
+ console.log(webhook);
118
+ return;
119
+ }
120
+
121
+ console.log(chalk.bold.blue('\nℹ️ Webhook Details'));
122
+ console.log(chalk.gray('━'.repeat(60)));
123
+
124
+ const fields = [
125
+ ['Name', webhook.hook_name],
126
+ ['Type', webhook.webhook_type],
127
+ ['URL', webhook.url],
128
+ ['Method', webhook.method],
129
+ ['Priority', webhook.priority],
130
+ ['Batch Name', webhook.batch_name],
131
+ ['Batch Order', webhook.batch_order],
132
+ ['Timeout', webhook.timeout],
133
+ ['Soft Timeout', webhook.soft_timeout],
134
+ ['Required', webhook.required ? 'Yes' : 'No'],
135
+ ['Active', webhook.active === false ? 'No' : 'Yes'] // Assuming default true if undefined
136
+ ];
137
+
138
+ fields.forEach(([label, value]) => {
139
+ console.log(` ${chalk.bold((label + ':').padEnd(20))} ${value !== undefined && value !== null ? value : '-'}`);
140
+ });
141
+ console.log('');
142
+
143
+ } catch (e) {
144
+ if (e.name === 'ExitPromptError') return;
145
+ handleError(e);
146
+ }
147
+ });
148
+
149
+ //-------------------------------------------------------
150
+ // Helper Functions
151
+ //-------------------------------------------------------
152
+
153
+ /**
154
+ * Parse JSON option string
155
+ */
156
+ function parseJsonOption(jsonString, fieldName) {
157
+ if (!jsonString) return undefined;
158
+ try {
159
+ return JSON.parse(jsonString);
160
+ } catch (e) {
161
+ throw new Error(`Invalid JSON for ${fieldName}: ${e.message}`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Collect fields interactively
167
+ */
168
+ async function collectFields() {
169
+ const fields = [];
170
+ const addFields = await confirm({ message: 'Add webhook fields?', default: false });
171
+ if (!addFields) return fields;
172
+
173
+ let addMore = true;
174
+ while (addMore) {
175
+ const name = await input({ message: 'Field name:', validate: v => v ? true : 'Required' });
176
+ const source = await input({ message: 'Field source:', validate: v => v ? true : 'Required' });
177
+ fields.push({ name, source });
178
+ addMore = await confirm({ message: 'Add another field?', default: false });
179
+ }
180
+ return fields;
181
+ }
182
+
183
+ /**
184
+ * Collect headers interactively
185
+ */
186
+ async function collectHeaders() {
187
+ const headers = [];
188
+ const addHeaders = await confirm({ message: 'Add webhook headers?', default: false });
189
+ if (!addHeaders) return headers;
190
+
191
+ let addMore = true;
192
+ while (addMore) {
193
+ const name = await input({ message: 'Header name:', validate: v => v ? true : 'Required' });
194
+ const value = await input({ message: 'Header value:', validate: v => v ? true : 'Required' });
195
+ headers.push({ name, value });
196
+ addMore = await confirm({ message: 'Add another header?', default: false });
197
+ }
198
+ return headers;
199
+ }
200
+
201
+ /**
202
+ * Collect rules interactively
203
+ */
204
+ async function collectRules() {
205
+ const rules = [];
206
+ const addRules = await confirm({ message: 'Add webhook rules?', default: false });
207
+ if (!addRules) return rules;
208
+
209
+ let addMore = true;
210
+ while (addMore) {
211
+ const field = await input({ message: 'Rule field:', validate: v => v ? true : 'Required' });
212
+ const operator = await select({
213
+ message: 'Operator:',
214
+ choices: [
215
+ { name: 'Equals (eq)', value: 'eq' },
216
+ { name: 'Not Equals (neq)', value: 'neq' },
217
+ { name: 'Greater Than (gt)', value: 'gt' },
218
+ { name: 'Less Than (lt)', value: 'lt' },
219
+ { name: 'Greater or Equal (gte)', value: 'gte' },
220
+ { name: 'Less or Equal (lte)', value: 'lte' }
221
+ ]
222
+ });
223
+ const value = await input({ message: 'Rule value:', validate: v => v ? true : 'Required' });
224
+ rules.push({ field, operator, value });
225
+ addMore = await confirm({ message: 'Add another rule?', default: false });
226
+ }
227
+ return rules;
228
+ }
229
+
230
+ //-------------------------------------------------------
231
+ // "webhook create" Command
232
+ //-------------------------------------------------------
233
+ webhooks.command('create')
234
+ .description('Subscribe to a new webhook')
235
+ .option('--name <name>', 'Hook Name')
236
+ .option('--webhook-method <method>', 'Webhook Method (e.g., observer.catalog_product_save_after)')
237
+ .option('--webhook-type <type>', 'Webhook Type (before/after)')
238
+ .option('--url <url>', 'Webhook URL')
239
+ .option('--method <method>', 'HTTP Method (POST/PUT/DELETE)')
240
+ .option('--batch-name <name>', 'Batch Name')
241
+ .option('--batch-order <n>', 'Batch Order')
242
+ .option('--priority <n>', 'Priority')
243
+ .option('--timeout <n>', 'Timeout in milliseconds')
244
+ .option('--soft-timeout <n>', 'Soft Timeout in milliseconds')
245
+ .option('--ttl <n>', 'Cache TTL')
246
+ .option('--fallback-error-message <msg>', 'Fallback Error Message')
247
+ .option('--required', 'Required')
248
+ .option('--fields <json>', 'Fields as JSON string')
249
+ .option('--headers <json>', 'Headers as JSON string')
250
+ .option('--rules <json>', 'Rules as JSON string')
251
+ .addHelpText('after', `
252
+ Examples:
253
+ # Interactive mode
254
+ $ mage-remote-run webhook create
255
+
256
+ # Create with all options
257
+ $ mage-remote-run webhook create \\
258
+ --name "Product Save Hook" \\
259
+ --webhook-method "observer.catalog_product_save_after" \\
260
+ --webhook-type "after" \\
261
+ --url https://example.com/webhook \\
262
+ --method POST \\
263
+ --timeout 5000 \\
264
+ --soft-timeout 3000 \\
265
+ --ttl 3600
266
+
267
+ # Create with fields, headers, and rules
268
+ $ mage-remote-run webhook create \\
269
+ --name "Order Complete" \\
270
+ --webhook-method "observer.sales_order_save_after" \\
271
+ --webhook-type "after" \\
272
+ --url https://example.com/orders \\
273
+ --fields '[{"name":"order_id","source":"order.entity_id"}]' \\
274
+ --headers '[{"name":"X-Auth","value":"token123"}]' \\
275
+ --rules '[{"field":"order.status","operator":"eq","value":"complete"}]'
276
+ `)
277
+ .action(async (options) => {
278
+ try {
279
+ const client = await createClient();
280
+
281
+ const name = options.name || await input({ message: 'Hook Name:', validate: v => v ? true : 'Required' });
282
+
283
+ let webhookMethod = options.webhookMethod;
284
+ if (!webhookMethod) {
285
+ // Fetch supported webhook types for interactive selection
286
+ let supportedTypes = [];
287
+ try {
288
+ const supportedData = await client.get('V1/webhooks/supportedList');
289
+ supportedTypes = Array.isArray(supportedData) ? supportedData.map(w => w.name) : [];
290
+ } catch (e) {
291
+ // If fetching fails, continue without suggestions
292
+ console.log(chalk.yellow('Warning: Could not fetch supported webhook types.'));
293
+ }
294
+
295
+ if (supportedTypes.length > 0) {
296
+ // Add custom option at the top
297
+ const CUSTOM_OPTION = '-- Enter custom webhook method --';
298
+
299
+ webhookMethod = await search({
300
+ message: 'Webhook Method:',
301
+ source: async (term) => {
302
+ const filtered = supportedTypes.filter(type =>
303
+ type.toLowerCase().includes((term || '').toLowerCase())
304
+ );
305
+ return [
306
+ { name: CUSTOM_OPTION, value: CUSTOM_OPTION },
307
+ ...filtered.map(type => ({ name: type, value: type }))
308
+ ];
309
+ }
310
+ });
311
+
312
+ // If custom option selected, prompt for manual input
313
+ if (webhookMethod === CUSTOM_OPTION) {
314
+ webhookMethod = await input({
315
+ message: 'Enter custom webhook method:',
316
+ validate: v => v ? true : 'Required'
317
+ });
318
+ }
319
+ } else {
320
+ // Fallback to regular input if no supported types available
321
+ webhookMethod = await input({
322
+ message: 'Webhook Method (e.g., observer.catalog_product_save_after):',
323
+ validate: v => v ? true : 'Required'
324
+ });
325
+ }
326
+ }
327
+
328
+ const webhookType = options.webhookType || await select({
329
+ message: 'Webhook Type:',
330
+ choices: [
331
+ { name: 'After', value: 'after' },
332
+ { name: 'Before', value: 'before' }
333
+ ],
334
+ default: 'after'
335
+ });
336
+
337
+ const url = options.url || await input({ message: 'Webhook URL:', validate: v => v ? true : 'Required' });
338
+
339
+ const method = options.method || await select({
340
+ message: 'HTTP Method:',
341
+ choices: [
342
+ { name: 'POST', value: 'POST' },
343
+ { name: 'PUT', value: 'PUT' },
344
+ { name: 'DELETE', value: 'DELETE' }
345
+ ],
346
+ default: 'POST'
347
+ });
348
+
349
+ // Batch name with default
350
+ const batchName = options.batchName || await input({
351
+ message: 'Batch Name:',
352
+ default: 'default'
353
+ });
354
+
355
+ // Batch order with default
356
+ const batchOrder = options.batchOrder || await input({
357
+ message: 'Batch Order:',
358
+ default: '0',
359
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number'
360
+ });
361
+
362
+ // Priority with default
363
+ const priority = options.priority || await input({
364
+ message: 'Priority:',
365
+ default: '0',
366
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number'
367
+ });
368
+
369
+ // Timeout with default
370
+ const timeout = options.timeout || await input({
371
+ message: 'Timeout (ms):',
372
+ default: '5000',
373
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number'
374
+ });
375
+
376
+ // Soft timeout with default
377
+ const softTimeout = options.softTimeout || await input({
378
+ message: 'Soft Timeout (ms):',
379
+ default: '3000',
380
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number'
381
+ });
382
+
383
+ // TTL with default
384
+ const ttl = options.ttl || await input({
385
+ message: 'Cache TTL:',
386
+ default: '3600',
387
+ validate: v => !isNaN(parseInt(v)) || 'Must be a number'
388
+ });
389
+
390
+ // Fallback error message
391
+ const fallbackErrorMessage = options.fallbackErrorMessage || await input({
392
+ message: 'Fallback Error Message:',
393
+ default: 'Webhook execution failed'
394
+ });
395
+
396
+ // Check if 'required' is explicitly passed
397
+ let isRequired = options.required;
398
+ if (isRequired === undefined) {
399
+ isRequired = await select({
400
+ message: 'Is Required?',
401
+ choices: [
402
+ { name: 'No', value: false },
403
+ { name: 'Yes', value: true }
404
+ ],
405
+ default: false
406
+ });
407
+ }
408
+
409
+ // Parse or collect fields, headers, rules
410
+ let fields = parseJsonOption(options.fields, 'fields');
411
+ let headers = parseJsonOption(options.headers, 'headers');
412
+ let rules = parseJsonOption(options.rules, 'rules');
413
+
414
+ // If not provided via options and we're in interactive mode, collect them
415
+ const isInteractive = !options.name || !options.webhookMethod || !options.url;
416
+ if (isInteractive) {
417
+ if (!fields) fields = await collectFields();
418
+ if (!headers) headers = await collectHeaders();
419
+ if (!rules) rules = await collectRules();
420
+ }
421
+
422
+ const payload = {
423
+ webhook: {
424
+ hook_name: name,
425
+ webhook_method: webhookMethod,
426
+ webhook_type: webhookType,
427
+ url: url,
428
+ method: method,
429
+ batch_name: batchName,
430
+ batch_order: parseInt(batchOrder),
431
+ priority: parseInt(priority),
432
+ timeout: parseInt(timeout),
433
+ soft_timeout: parseInt(softTimeout),
434
+ ttl: parseInt(ttl),
435
+ fallback_error_message: fallbackErrorMessage,
436
+ required: isRequired,
437
+ fields: fields || [],
438
+ headers: headers || [],
439
+ rules: rules || []
440
+ }
441
+ };
442
+
443
+ // Filter undefined
444
+ Object.keys(payload.webhook).forEach(key => payload.webhook[key] === undefined && delete payload.webhook[key]);
445
+
446
+ await client.post('V1/webhooks/subscribe', payload);
447
+
448
+ console.log(chalk.green(`\n✅ Webhook "${name}" created (subscribed) successfully!`));
449
+ } catch (e) {
450
+ if (e.name === 'ExitPromptError') return;
451
+ handleError(e);
452
+ }
453
+ });
454
+
455
+ //-------------------------------------------------------
456
+ // "webhook delete" Command
457
+ //-------------------------------------------------------
458
+ webhooks.command('delete')
459
+ .description('Unsubscribe from a webhook')
460
+ .argument('[name]', 'Webhook Name')
461
+ .action(async (name) => {
462
+ try {
463
+ const client = await createClient();
464
+
465
+ const listData = await client.get('V1/webhooks/list');
466
+ const items = Array.isArray(listData) ? listData : [];
467
+
468
+ if (!name) {
469
+ if (items.length === 0) {
470
+ console.log(chalk.yellow('No webhooks found.'));
471
+ return;
472
+ }
473
+ name = await select({
474
+ message: 'Select Webhook to Delete:',
475
+ choices: items.map(w => ({
476
+ name: `${w.hook_name} (${w.webhook_type})`,
477
+ value: w.hook_name
478
+ }))
479
+ });
480
+ }
481
+
482
+ const webhook = items.find(w => w.hook_name === name);
483
+ if (!webhook) throw new Error(`Webhook "${name}" not found.`);
484
+
485
+ const shouldDelete = await confirm({ message: `Are you sure you want to delete webhook "${name}"?`, default: false });
486
+ if (!shouldDelete) return;
487
+
488
+ // Unsubscribe requires sending the webhook object wrapper
489
+ await client.post('V1/webhooks/unsubscribe', { webhook });
490
+ console.log(chalk.green(`\n✅ Webhook "${name}" deleted (unsubscribed) successfully.`));
491
+ } catch (e) {
492
+ if (e.name === 'ExitPromptError') return;
493
+ handleError(e);
494
+ }
495
+ });
57
496
  }
package/lib/utils.js CHANGED
@@ -11,7 +11,33 @@ export function printTable(headers, data) {
11
11
  }
12
12
 
13
13
  export function handleError(error) {
14
- console.error(chalk.red('Error:'), error.message);
14
+ let message = error.message;
15
+
16
+ // specific handling for Magento API Errors which are often JSON stringified
17
+ // Format: "API Error 404: {...}"
18
+ const apiErrorMatch = message.match(/^API Error (\d+): (.+)$/);
19
+ if (apiErrorMatch) {
20
+ const statusCode = apiErrorMatch[1];
21
+ const jsonPart = apiErrorMatch[2];
22
+ try {
23
+ const parsed = JSON.parse(jsonPart);
24
+ if (parsed.message) {
25
+ let prettyMessage = parsed.message;
26
+ if (parsed.parameters) {
27
+ // Substitute %fieldName with values
28
+ Object.keys(parsed.parameters).forEach(key => {
29
+ prettyMessage = prettyMessage.replace(new RegExp(`%${key}`, 'g'), parsed.parameters[key]);
30
+ });
31
+ }
32
+ message = `${prettyMessage}`;
33
+ // Optional: append status code if not 200? The user request just showed the clean message.
34
+ }
35
+ } catch (e) {
36
+ // If parsing fails, keep original message
37
+ }
38
+ }
39
+
40
+ console.error(chalk.red('Error:'), message);
15
41
  if (process.env.DEBUG) {
16
42
  console.error(error);
17
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -32,6 +32,7 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "@inquirer/prompts": "^8.1.0",
35
+ "@inquirer/search": "^4.0.3",
35
36
  "@modelcontextprotocol/sdk": "^1.25.1",
36
37
  "axios": "^1.13.2",
37
38
  "chalk": "^5.6.2",