mage-remote-run 0.16.0 → 0.18.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.
@@ -10,6 +10,7 @@ import { registerTaxCommands } from './commands/tax.js';
10
10
  import { registerInventoryCommands } from './commands/inventory.js';
11
11
  import { registerAdobeIoEventsCommands } from './commands/adobe-io-events.js';
12
12
  import { registerWebhooksCommands } from './commands/webhooks.js';
13
+ import { registerPurchaseOrderCartCommands } from './commands/purchase-order-cart.js';
13
14
  import { registerConsoleCommand } from './commands/console.js';
14
15
 
15
16
  export { registerConnectionCommands, registerConsoleCommand };
@@ -29,6 +30,7 @@ export function registerCoreCommands(program) {
29
30
  export function registerCloudCommands(program) {
30
31
  registerAdobeIoEventsCommands(program);
31
32
  registerCompanyCommands(program);
33
+ registerPurchaseOrderCartCommands(program);
32
34
  registerWebhooksCommands(program);
33
35
  }
34
36
 
@@ -132,4 +132,439 @@ Examples:
132
132
 
133
133
  } catch (e) { handleError(e); }
134
134
  });
135
+
136
+ //-------------------------------------------------------
137
+ // "company create" Command
138
+ //-------------------------------------------------------
139
+ company.command('create')
140
+ .description('Create a new company')
141
+ .addHelpText('after', `
142
+ Examples:
143
+ $ mage-remote-run company create
144
+ `)
145
+ .action(async () => {
146
+ try {
147
+ const client = await createClient();
148
+ const { default: inquirer } = await import('inquirer');
149
+
150
+ // Prompt for basic info
151
+ const answers = await inquirer.prompt([
152
+ { name: 'company_name', message: 'Company Name:' },
153
+ { name: 'company_email', message: 'Company Email:' },
154
+ { name: 'legal_name', message: 'Legal Name (optional):' },
155
+ { name: 'vat_tax_id', message: 'VAT/Tax ID (optional):' },
156
+ { name: 'reseller_id', message: 'Reseller ID (optional):' },
157
+ { name: 'customer_group_id', message: 'Customer Group ID:', default: '1' },
158
+ { name: 'sales_representative_id', message: 'Sales Representative ID (Admin User ID):', default: '1' },
159
+ { name: 'email', message: 'Super Admin Email:' },
160
+ { name: 'firstname', message: 'Super Admin First Name:' },
161
+ { name: 'lastname', message: 'Super Admin Last Name:' }
162
+ ]);
163
+
164
+ // Address is required usually
165
+ const address = await inquirer.prompt([
166
+ { name: 'street', message: 'Street:' },
167
+ { name: 'city', message: 'City:' },
168
+ { name: 'country_id', message: 'Country ID:', default: 'US' },
169
+ { name: 'region', message: 'Region/State:' },
170
+ { name: 'postcode', message: 'Postcode:' },
171
+ { name: 'telephone', message: 'Telephone:' }
172
+ ]);
173
+
174
+ const payload = {
175
+ company: {
176
+ company_name: answers.company_name,
177
+ company_email: answers.company_email,
178
+ legal_name: answers.legal_name,
179
+ vat_tax_id: answers.vat_tax_id,
180
+ reseller_id: answers.reseller_id,
181
+ customer_group_id: answers.customer_group_id,
182
+ sales_representative_id: answers.sales_representative_id,
183
+ super_user_id: 0, // 0 for new user
184
+ street: [address.street],
185
+ city: address.city,
186
+ country_id: address.country_id,
187
+ region: { region: address.region }, // Simplified
188
+ postcode: address.postcode,
189
+ telephone: address.telephone
190
+ },
191
+ company_admin: {
192
+ email: answers.email,
193
+ firstname: answers.firstname,
194
+ lastname: answers.lastname
195
+ }
196
+ };
197
+
198
+ // POST /V1/company/
199
+ const data = await client.post('V1/company', payload);
200
+ console.log(chalk.green(`✅ Company created with ID: ${data.id}`));
201
+ } catch (e) { handleError(e); }
202
+ });
203
+
204
+ //-------------------------------------------------------
205
+ // "company update" Command
206
+ //-------------------------------------------------------
207
+ company.command('update <companyId>')
208
+ .description('Update company details')
209
+ .addHelpText('after', `
210
+ Examples:
211
+ $ mage-remote-run company update 123
212
+ `)
213
+ .action(async (companyId) => {
214
+ try {
215
+ const client = await createClient();
216
+ const { default: inquirer } = await import('inquirer');
217
+
218
+ // First fetch data
219
+ let current;
220
+ try {
221
+ current = await client.get(`V1/company/${companyId}`);
222
+ } catch (e) {
223
+ throw new Error(`Company ${companyId} not found.`);
224
+ }
225
+
226
+ const answers = await inquirer.prompt([
227
+ { name: 'company_name', message: 'Company Name:', default: current.company_name },
228
+ { name: 'company_email', message: 'Company Email:', default: current.company_email },
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
+ ]);
232
+
233
+ // Merge
234
+ const payload = {
235
+ company: {
236
+ ...current,
237
+ ...answers
238
+ }
239
+ };
240
+
241
+ await client.put(`V1/company/${companyId}`, payload);
242
+ console.log(chalk.green(`✅ Company ${companyId} updated.`));
243
+ } catch (e) { handleError(e); }
244
+ });
245
+
246
+ //-------------------------------------------------------
247
+ // "company delete" Command
248
+ //-------------------------------------------------------
249
+ company.command('delete <companyId>')
250
+ .description('Delete a company')
251
+ .option('--force', 'Force delete without confirmation')
252
+ .action(async (companyId, options) => {
253
+ try {
254
+ const client = await createClient();
255
+ const { default: inquirer } = await import('inquirer');
256
+
257
+ if (!options.force) {
258
+ const { confirm } = await inquirer.prompt([{
259
+ type: 'confirm',
260
+ name: 'confirm',
261
+ message: `Delete company ${companyId}?`,
262
+ default: false
263
+ }]);
264
+ if (!confirm) return;
265
+ }
266
+
267
+ await client.delete(`V1/company/${companyId}`);
268
+ console.log(chalk.green(`✅ Company ${companyId} deleted.`));
269
+ } catch (e) { handleError(e); }
270
+ });
271
+
272
+ //-------------------------------------------------------
273
+ // "company structure" Command
274
+ //-------------------------------------------------------
275
+ company.command('structure <companyId>')
276
+ .description('Show company structure (hierarchy)')
277
+ .action(async (companyId) => {
278
+ try {
279
+ const client = await createClient();
280
+ // GET /V1/company/relations usually returns the user structure.
281
+ // However, without a specific hierarchy endpoint that is easy to visualize, we might just dump data.
282
+ const data = await client.get(`V1/company/${companyId}`);
283
+ console.log(chalk.bold(`Company Structure for ${data.company_name} (${data.id})`));
284
+ console.log('Hierarchy visualization requires more complex data. Showing basic info.');
285
+ console.log(`Parent ID: ${data.parent_id || 'None'}`);
286
+ console.log(`Legal Name: ${data.legal_name || '-'}`);
287
+ } catch (e) { handleError(e); }
288
+ });
289
+
290
+ //-------------------------------------------------------
291
+ // "company role" Group
292
+ //-------------------------------------------------------
293
+ const role = company.command('role').description('Manage company roles');
294
+
295
+ role.command('list')
296
+ .description('List roles')
297
+ .action(async () => {
298
+ try {
299
+ const client = await createClient();
300
+ // Assuming lists all roles visible to admin context
301
+ const data = await client.get('V1/company/role', { 'searchCriteria[pageSize]': 20 });
302
+ const items = data.items || [];
303
+ const rows = items.map(r => [r.role_id, r.role_name, r.company_id]);
304
+ console.log(chalk.bold(`Total Roles: ${data.total_count}`));
305
+ printTable(['ID', 'Name', 'Company ID'], rows);
306
+ } catch (e) { handleError(e); }
307
+ });
308
+
309
+ role.command('show <roleId>')
310
+ .description('Show role details')
311
+ .action(async (roleId) => {
312
+ try {
313
+ const client = await createClient();
314
+ const data = await client.get(`V1/company/role/${roleId}`);
315
+ console.log(chalk.bold(`\nRole: ${data.role_name} (ID: ${data.role_id})`));
316
+ if (data.permissions) {
317
+ console.log(chalk.bold('\nPermissions:'));
318
+ data.permissions.forEach(p => console.log(` - ${p.resource_id}: ${p.permission}`));
319
+ }
320
+ } catch (e) { handleError(e); }
321
+ });
322
+
323
+ //-------------------------------------------------------
324
+ // "company credit" Group
325
+ //-------------------------------------------------------
326
+ const credit = company.command('credit').description('Manage company credits');
327
+
328
+ credit.command('show <companyId>')
329
+ .description('Show credit for company')
330
+ .action(async (companyId) => {
331
+ try {
332
+ const client = await createClient();
333
+ const data = await client.get(`V1/companyCredits/company/${companyId}`);
334
+ console.log(chalk.bold(`\nCredit ID: ${data.id}`));
335
+ console.log(`Balance: ${data.balance} ${data.currency_code}`);
336
+ console.log(`Limit: ${data.credit_limit} ${data.currency_code}`);
337
+ console.log(`Avail: ${data.available_limit} ${data.currency_code}`);
338
+ } catch (e) { handleError(e); }
339
+ });
340
+
341
+ credit.command('history <companyId>')
342
+ .description('Show credit history')
343
+ .option('-p, --page <number>', 'Page number', '1')
344
+ .action(async (companyId, options) => {
345
+ try {
346
+ const client = await createClient();
347
+
348
+ // 1. Get Credit ID for the company
349
+ let creditData;
350
+ try {
351
+ creditData = await client.get(`V1/companyCredits/company/${companyId}`);
352
+ } catch (e) {
353
+ throw new Error(`Could not find credit profile for company ${companyId}`);
354
+ }
355
+ const creditId = creditData.id;
356
+
357
+ // 2. Search History by company_credit_id
358
+ const params = {
359
+ 'searchCriteria[filterGroups][0][filters][0][field]': 'company_credit_id',
360
+ 'searchCriteria[filterGroups][0][filters][0][value]': creditId,
361
+ 'searchCriteria[currentPage]': options.page,
362
+ 'searchCriteria[pageSize]': 20
363
+ };
364
+ const data = await client.get('V1/companyCredits/history', params);
365
+
366
+ if (data.total_count === 0) {
367
+ console.log(chalk.yellow('No history found.'));
368
+ return;
369
+ }
370
+
371
+ const typeMap = {
372
+ 1: 'Allocate',
373
+ 2: 'Reimburse',
374
+ 3: 'Purchase',
375
+ 4: 'Refund',
376
+ 5: 'Revert',
377
+ 6: 'Currency Update'
378
+ };
379
+ const rows = (data.items || []).map(h => [
380
+ h.datetime,
381
+ `${typeMap[h.type] || 'Unknown'} (${h.type})`,
382
+ h.amount,
383
+ h.balance,
384
+ h.comment
385
+ ]);
386
+ console.log(chalk.bold(`Total Entries: ${data.total_count}`));
387
+ printTable(['Date', 'Type', 'Amount', 'Balance', 'Comment'], rows);
388
+ } catch (e) { handleError(e); }
389
+ });
390
+
391
+ credit.command('increase [creditId] [amount]')
392
+ .description('Increase balance')
393
+ .option('--currency <code >', 'Currency code')
394
+ .option('--type <number>', 'Operation Type (1=Allocate, 2=Reimburse, 4=Refund, 5=Revert)')
395
+ .option('--comment <text>', 'Comment', 'Manual increase')
396
+ .option('--po <number>', 'PO Number', '')
397
+ .action(async (creditId, amount, options) => {
398
+ try {
399
+ const client = await createClient();
400
+ const inquirer = (await import('inquirer')).default;
401
+ const { select } = await import('@inquirer/prompts');
402
+
403
+ let isInteractive = false;
404
+ if (!creditId) {
405
+ isInteractive = true;
406
+ const ans = await inquirer.prompt([{
407
+ type: 'input',
408
+ name: 'creditId',
409
+ message: 'Enter Credit ID:'
410
+ }]);
411
+ creditId = ans.creditId;
412
+ }
413
+
414
+ if (!amount) {
415
+ isInteractive = true;
416
+ const ans = await inquirer.prompt([{
417
+ type: 'input',
418
+ name: 'amount',
419
+ message: 'Enter Amount:'
420
+ }]);
421
+ amount = ans.amount;
422
+ }
423
+
424
+ // Operation Type Selection (if not provided)
425
+ let operationType = options.type;
426
+ if (!operationType) {
427
+ if (isInteractive) {
428
+ operationType = await select({
429
+ message: 'Select Operation Type:',
430
+ choices: [
431
+ { name: 'Reimburse (2)', value: 2 },
432
+ { name: 'Allocate (1)', value: 1 },
433
+ { name: 'Refund (4)', value: 4 },
434
+ { name: 'Revert (5)', value: 5 }
435
+ ],
436
+ default: 2
437
+ });
438
+ } else {
439
+ operationType = 2;
440
+ }
441
+ }
442
+
443
+ // 1. Get current credit details to find currency
444
+ let currency = options.currency;
445
+ if (!currency) {
446
+ try {
447
+ const creditData = await client.get(`V1/companyCredits/${creditId}`);
448
+ currency = creditData.currency_code;
449
+ } catch (e) {
450
+ console.warn(chalk.yellow('Could not fetch credit details to determine currency. Using interactive prompt.'));
451
+ }
452
+ }
453
+
454
+ if (!currency) {
455
+ const ans = await inquirer.prompt([{
456
+ type: 'input',
457
+ name: 'currency',
458
+ message: 'Enter currency code:',
459
+ default: 'USD'
460
+ }]);
461
+ currency = ans.currency;
462
+ }
463
+
464
+ const payload = {
465
+ value: parseFloat(amount),
466
+ currency: currency,
467
+ operationType: parseInt(operationType), // 1=Allocate, 2=Reimburse, 4=Refund, 5=Revert
468
+ comment: options.comment,
469
+ options: {
470
+ purchase_order: options.po,
471
+ order_increment: '',
472
+ currency_display: currency,
473
+ currency_base: currency
474
+ }
475
+ };
476
+
477
+ await client.post(`V1/companyCredits/${creditId}/increaseBalance`, payload);
478
+ console.log(chalk.green(`✅ Credit ${creditId} increased by ${amount} ${currency}`));
479
+ } catch (e) { handleError(e); }
480
+ });
481
+
482
+ credit.command('decrease [creditId] [amount]')
483
+ .description('Decrease balance')
484
+ .option('--currency <code>', 'Currency code')
485
+ .option('--type <number>', 'Operation Type (3=Purchase)')
486
+ .option('--comment <text>', 'Comment', 'Manual decrease')
487
+ .option('--po <number>', 'PO Number', '')
488
+ .action(async (creditId, amount, options) => {
489
+ try {
490
+ const client = await createClient();
491
+ const inquirer = (await import('inquirer')).default;
492
+ const { select } = await import('@inquirer/prompts');
493
+
494
+ let isInteractive = false;
495
+ if (!creditId) {
496
+ isInteractive = true;
497
+ const ans = await inquirer.prompt([{
498
+ type: 'input',
499
+ name: 'creditId',
500
+ message: 'Enter Credit ID:'
501
+ }]);
502
+ creditId = ans.creditId;
503
+ }
504
+
505
+ if (!amount) {
506
+ isInteractive = true;
507
+ const ans = await inquirer.prompt([{
508
+ type: 'input',
509
+ name: 'amount',
510
+ message: 'Enter Amount:'
511
+ }]);
512
+ amount = ans.amount;
513
+ }
514
+
515
+ // Operation Type Selection (if not provided)
516
+ let operationType = options.type;
517
+ if (!operationType) {
518
+ if (isInteractive) {
519
+ operationType = await select({
520
+ message: 'Select Operation Type:',
521
+ choices: [
522
+ { name: 'Purchase (3)', value: 3 },
523
+ { name: 'Reimburse (2)', value: 2 }
524
+ ],
525
+ default: 3
526
+ });
527
+ } else {
528
+ operationType = 3;
529
+ }
530
+ }
531
+
532
+ // 1. Get current credit details to find currency
533
+ let currency = options.currency;
534
+ if (!currency) {
535
+ try {
536
+ const creditData = await client.get(`V1/companyCredits/${creditId}`);
537
+ currency = creditData.currency_code;
538
+ } catch (e) {
539
+ console.warn(chalk.yellow('Could not fetch credit details to determine currency. Using interactive prompt.'));
540
+ }
541
+ }
542
+
543
+ if (!currency) {
544
+ const ans = await inquirer.prompt([{
545
+ type: 'input',
546
+ name: 'currency',
547
+ message: 'Enter currency code:',
548
+ default: 'USD'
549
+ }]);
550
+ currency = ans.currency;
551
+ }
552
+
553
+ const payload = {
554
+ value: parseFloat(amount),
555
+ currency: currency,
556
+ operationType: parseInt(operationType),
557
+ comment: options.comment,
558
+ options: {
559
+ purchase_order: options.po,
560
+ order_increment: '',
561
+ currency_display: currency,
562
+ currency_base: currency
563
+ }
564
+ };
565
+
566
+ await client.post(`V1/companyCredits/${creditId}/decreaseBalance`, payload);
567
+ console.log(chalk.green(`✅ Credit ${creditId} decreased by ${amount} ${currency}`));
568
+ } catch (e) { handleError(e); }
569
+ });
135
570
  }
@@ -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;
@@ -0,0 +1,63 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { printTable, handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+
5
+ export function registerPurchaseOrderCartCommands(program) {
6
+ const poCart = program.command('po-cart').description('Manage Purchase Order Carts');
7
+
8
+ //-------------------------------------------------------
9
+ // "po-cart totals" Command
10
+ //-------------------------------------------------------
11
+ poCart.command('totals <cartId>')
12
+ .description('Get purchase order cart totals')
13
+ .action(async (cartId) => {
14
+ try {
15
+ const client = await createClient();
16
+ const data = await client.get(`V1/purchase-order-carts/${cartId}/totals`);
17
+
18
+ console.log(chalk.bold('Totals:'));
19
+ const rows = (data.total_segments || []).map(t => [t.title, t.value]);
20
+ printTable(['Title', 'Value'], rows);
21
+
22
+ console.log(chalk.bold(`\nGrand Total: ${data.grand_total} ${data.quote_currency_code}`));
23
+ } catch (e) { handleError(e); }
24
+ });
25
+
26
+ //-------------------------------------------------------
27
+ // "po-cart shipping-methods" Command
28
+ //-------------------------------------------------------
29
+ poCart.command('shipping-methods <cartId>')
30
+ .description('Estimate shipping methods (requires address ID)')
31
+ .requiredOption('--address-id <id>', 'Address ID to estimate for')
32
+ .action(async (cartId, options) => {
33
+ try {
34
+ const client = await createClient();
35
+ const payload = { addressId: options.addressId };
36
+ const data = await client.post(`V1/purchase-order-carts/${cartId}/estimate-shipping-methods-by-address-id`, payload);
37
+
38
+ console.log(chalk.bold('Shipping Methods:'));
39
+ const rows = data.map(m => [m.carrier_title, m.method_title, m.amount, m.price_excl_tax]);
40
+ printTable(['Carrier', 'Method', 'Amount', 'Price Excl Tax'], rows);
41
+ } catch (e) { handleError(e); }
42
+ });
43
+
44
+ //-------------------------------------------------------
45
+ // "po-cart payment-info" Command
46
+ //-------------------------------------------------------
47
+ poCart.command('payment-info <cartId>')
48
+ .description('Get payment information')
49
+ .action(async (cartId) => {
50
+ try {
51
+ const client = await createClient();
52
+ const data = await client.get(`V1/purchase-order-carts/${cartId}/payment-information`);
53
+
54
+ console.log(chalk.bold('Payment Methods:'));
55
+ const methods = data.payment_methods || [];
56
+ methods.forEach(m => console.log(`- ${m.title} (${m.code})`));
57
+
58
+ console.log(chalk.bold('\nTotals:'));
59
+ const totals = data.totals || {};
60
+ console.log(`Grand Total: ${totals.grand_total} ${totals.quote_currency_code}`);
61
+ } catch (e) { handleError(e); }
62
+ });
63
+ }
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.16.0",
3
+ "version": "0.18.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": {