mage-remote-run 0.24.0 → 0.26.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/api/paas.js CHANGED
@@ -40,15 +40,21 @@ export class PaasClient extends ApiClient {
40
40
  const baseURL = config.baseURL || '';
41
41
  const url = baseURL.replace(/\/$/, '') + '/' + config.url.replace(/^\//, ''); // rudimentary join
42
42
 
43
+ // Determine signature method (default to HMAC-SHA256)
44
+ const signatureMethod = (this.config.auth.signatureMethod || 'hmac-sha256').toLowerCase();
45
+ const isSha1 = signatureMethod === 'hmac-sha1' || signatureMethod === 'sha1';
46
+ const methodStr = isSha1 ? 'HMAC-SHA1' : 'HMAC-SHA256';
47
+ const hashAlgo = isSha1 ? 'sha1' : 'sha256';
48
+
43
49
  const oauth = OAuth({
44
50
  consumer: {
45
51
  key: this.config.auth.consumerKey,
46
52
  secret: this.config.auth.consumerSecret
47
53
  },
48
- signature_method: 'HMAC-SHA256',
54
+ signature_method: methodStr,
49
55
  hash_function(base_string, key) {
50
56
  return crypto
51
- .createHmac('sha256', key)
57
+ .createHmac(hashAlgo, key)
52
58
  .update(base_string)
53
59
  .digest('base64');
54
60
  }
@@ -16,8 +16,9 @@ import { registerImportCommands } from './commands/import.js';
16
16
  import { registerModulesCommands } from './commands/modules.js';
17
17
  import { registerConsoleCommand } from './commands/console.js';
18
18
  import { registerShipmentCommands } from './commands/shipments.js';
19
+ import { registerRestCommands } from './commands/rest.js';
19
20
 
20
- export { registerConnectionCommands, registerConsoleCommand, registerShipmentCommands };
21
+ export { registerConnectionCommands, registerConsoleCommand, registerShipmentCommands, registerRestCommands };
21
22
 
22
23
  const GROUPS = {
23
24
  CORE: [
@@ -31,7 +32,8 @@ const GROUPS = {
31
32
  registerTaxCommands,
32
33
  registerInventoryCommands,
33
34
  registerShipmentCommands,
34
- registerConsoleCommand
35
+ registerConsoleCommand,
36
+ registerRestCommands
35
37
  ],
36
38
  COMMERCE: [
37
39
  registerCompanyCommands,
@@ -7,6 +7,22 @@ import inquirer from 'inquirer';
7
7
  import chalk from 'chalk';
8
8
  import { getMissingB2BModules } from '../b2b.js';
9
9
 
10
+ // Helper to test connection (non-interactive or one-shot)
11
+ async function testConnection(name, settings) {
12
+ console.log(chalk.blue(`\nTesting connection "${name}"...`));
13
+ try {
14
+ const client = await createClient(settings);
15
+ const start = Date.now();
16
+ await client.get('V1/store/storeViews');
17
+ const duration = Date.now() - start;
18
+ console.log(chalk.green(`✔ Connection successful! (${duration}ms)`));
19
+ return { success: true };
20
+ } catch (e) {
21
+ console.error(chalk.red(`✖ Connection failed: ${e.message}`));
22
+ return { success: false, error: e };
23
+ }
24
+ }
25
+
10
26
  // Helper to handle interactive connection configuration and testing
11
27
  async function configureAndTestConnection(name, initialSettings = {}) {
12
28
  let settings = await askForProfileSettings(initialSettings);
@@ -19,18 +35,13 @@ async function configureAndTestConnection(name, initialSettings = {}) {
19
35
  });
20
36
 
21
37
  if (shouldTest) {
22
- console.log(chalk.blue(`\nTesting connection "${name}"...`));
23
- try {
24
- const client = await createClient(settings);
25
- const start = Date.now();
26
- await client.get('V1/store/storeViews');
27
- const duration = Date.now() - start;
28
- console.log(chalk.green(`✔ Connection successful! (${duration}ms)`));
38
+ const result = await testConnection(name, settings);
39
+
40
+ if (result.success) {
29
41
  lastTestError = null;
30
42
  break; // Test passed, proceed to save
31
- } catch (e) {
32
- console.error(chalk.red(`✖ Connection failed: ${e.message}`));
33
- lastTestError = e;
43
+ } else {
44
+ lastTestError = result.error;
34
45
  const shouldEdit = await confirm({
35
46
  message: 'Connection failed. Do you want to change settings?',
36
47
  default: true
@@ -345,41 +356,152 @@ export function registerConnectionCommands(program) {
345
356
  //-------------------------------------------------------
346
357
  connections.command('add')
347
358
  .description('Configure a new connection profile')
359
+ .option('--name <name>', 'Profile Name')
360
+ .option('--type <type>', 'System Type (magento-os, mage-os, ac-on-prem, ac-cloud-paas, ac-saas)')
361
+ .option('--url <url>', 'Instance URL')
362
+ .option('--client-id <id>', 'Client ID (SaaS)')
363
+ .option('--client-secret <secret>', 'Client Secret (SaaS)')
364
+ .option('--auth-method <method>', 'Auth Method (bearer, oauth1)')
365
+ .option('--token <token>', 'Bearer Token')
366
+ .option('--consumer-key <key>', 'Consumer Key (OAuth1)')
367
+ .option('--consumer-secret <secret>', 'Consumer Secret (OAuth1)')
368
+ .option('--access-token <token>', 'Access Token (OAuth1)')
369
+ .option('--token-secret <secret>', 'Token Secret (OAuth1)')
370
+ .option('--signature-method <method>', 'Signature Method (hmac-sha256, hmac-sha1)', 'hmac-sha256')
371
+ .option('--active', 'Set as active profile')
372
+ .option('--no-test', 'Skip connection test')
348
373
  .addHelpText('after', `
349
374
  Examples:
375
+ Interactive Mode:
350
376
  $ mage-remote-run connection add
377
+
378
+ SaaS (Non-Interactive):
379
+ $ mage-remote-run connection add --name "MySaaS" --type ac-saas --url "https://example.com" --client-id "id" --client-secret "secret" --active
380
+
381
+ Bearer Token (Non-Interactive):
382
+ $ mage-remote-run connection add --name "MyStore" --type magento-os --url "https://magento.example.com" --token "tkn"
351
383
  `)
352
- .action(async () => {
384
+ .action(async (options) => {
353
385
  console.log(chalk.blue('Configure a new connection Profile'));
354
386
  try {
355
- const name = await input({
356
- message: 'Profile Name:',
357
- validate: value => value ? true : 'Name is required'
358
- });
387
+ let name = options.name;
388
+ let settings = null;
359
389
 
360
- const settings = await configureAndTestConnection(name);
361
- if (!settings) {
362
- console.log(chalk.yellow('\nConfiguration cancelled.'));
363
- return;
390
+ // Non-Interactive Mode
391
+ if (options.type) {
392
+ if (!name) {
393
+ throw new Error('Option --name is required when using --type');
394
+ }
395
+ if (!options.url) {
396
+ throw new Error('Option --url is required when using --type');
397
+ }
398
+
399
+ settings = {
400
+ type: options.type,
401
+ url: options.url,
402
+ auth: {}
403
+ };
404
+
405
+ if (options.type === 'ac-saas') {
406
+ if (!options.clientId || !options.clientSecret) {
407
+ throw new Error('SaaS authentication requires --client-id and --client-secret');
408
+ }
409
+ settings.auth = {
410
+ clientId: options.clientId,
411
+ clientSecret: options.clientSecret
412
+ };
413
+ } else {
414
+ // Infer auth method if not provided
415
+ let method = options.authMethod;
416
+ if (!method) {
417
+ if (options.token) {
418
+ method = 'bearer';
419
+ } else if (options.consumerKey && options.consumerSecret) {
420
+ method = 'oauth1';
421
+ }
422
+ }
423
+
424
+ if (method === 'bearer') {
425
+ if (!options.token) {
426
+ throw new Error('Bearer authentication requires --token');
427
+ }
428
+ settings.auth = {
429
+ method: 'bearer',
430
+ token: options.token
431
+ };
432
+ } else if (method === 'oauth1') {
433
+ if (!options.consumerKey || !options.consumerSecret || !options.accessToken || !options.tokenSecret) {
434
+ throw new Error('OAuth1 authentication requires --consumer-key, --consumer-secret, --access-token, and --token-secret');
435
+ }
436
+ settings.auth = {
437
+ method: 'oauth1',
438
+ consumerKey: options.consumerKey,
439
+ consumerSecret: options.consumerSecret,
440
+ accessToken: options.accessToken,
441
+ tokenSecret: options.tokenSecret,
442
+ signatureMethod: options.signatureMethod
443
+ };
444
+ } else {
445
+ throw new Error('Invalid or missing authentication options. Use --token for Bearer or provide OAuth keys.');
446
+ }
447
+ }
448
+
449
+ // Test connection if not skipped
450
+ let lastTestError = null;
451
+ if (options.test !== false) { // --no-test sets options.test to false
452
+ const result = await testConnection(name, settings);
453
+ if (!result.success) {
454
+ throw new Error(`Connection test failed: ${result.error.message}`);
455
+ }
456
+ }
457
+
458
+ settings = await updateProfileCapabilities(settings, lastTestError);
459
+
460
+ } else {
461
+ // Interactive Mode
462
+ name = await input({
463
+ message: 'Profile Name:',
464
+ validate: value => value ? true : 'Name is required'
465
+ });
466
+
467
+ settings = await configureAndTestConnection(name);
468
+ if (!settings) {
469
+ console.log(chalk.yellow('\nConfiguration cancelled.'));
470
+ return;
471
+ }
364
472
  }
365
473
 
366
474
  await addProfile(name, settings);
367
475
  console.log(chalk.green(`\nProfile "${name}" saved successfully!`));
368
476
 
369
- // Ask to set as active if multiple exist
370
477
  const config = await loadConfig();
371
- if (Object.keys(config.profiles).length > 1) {
372
- const setActive = await confirm({
373
- message: 'Set this as the active profile?',
374
- default: true
375
- });
376
-
377
- if (setActive) {
378
- config.activeProfile = name;
379
- await saveConfig(config);
380
- console.log(chalk.green(`Profile "${name}" set as active.`));
478
+
479
+ let setActive = false;
480
+ if (options.type) {
481
+ // Non-interactive: only set active if requested or if it's the only one (handled by addProfile implicitly for first one, but we check logic here)
482
+ if (options.active) {
483
+ setActive = true;
381
484
  }
485
+ } else {
486
+ // Interactive
487
+ if (Object.keys(config.profiles).length > 1) {
488
+ setActive = await confirm({
489
+ message: 'Set this as the active profile?',
490
+ default: true
491
+ });
492
+ }
493
+ }
494
+
495
+ // addProfile already sets active if it's the first profile.
496
+ // We only need to force it if requested or confirmed and it wasn't already set (e.g. multiple profiles)
497
+ // Actually addProfile sets it if !activeProfile.
498
+
499
+ if (setActive && config.activeProfile !== name) {
500
+ config.activeProfile = name;
501
+ await saveConfig(config);
502
+ console.log(chalk.green(`Profile "${name}" set as active.`));
382
503
  }
504
+
383
505
  } catch (e) {
384
506
  if (e.name === 'ExitPromptError') {
385
507
  console.log(chalk.yellow('\nConfiguration cancelled.'));
@@ -0,0 +1,136 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+ import { input, select, editor } from '@inquirer/prompts';
5
+
6
+ export function registerRestCommands(program) {
7
+ program.command('rest [path]')
8
+ .description('Execute a manual REST API request')
9
+ .option('-m, --method <method>', 'HTTP Method (GET, POST, PUT, DELETE)')
10
+ .option('-d, --data <data>', 'Request body data (JSON)')
11
+ .option('-q, --query <string>', 'Query parameters (e.g. "a=1&b=2")')
12
+ .option('--page-size <number>', 'Search Criteria Page Size')
13
+ .option('--current-page <number>', 'Search Criteria Current Page')
14
+ .option('-c, --content-type <type>', 'Content-Type', 'application/json')
15
+ .option('-f, --format <type>', 'Output format (json, xml)')
16
+ .addHelpText('after', `
17
+ Examples:
18
+ $ mage-remote-run rest V1/store/websites
19
+ $ mage-remote-run rest V1/customers/1 -m GET
20
+ $ mage-remote-run rest V1/customers -m POST -d '{"customer": {"email": "test@example.com", ...}}'
21
+ $ mage-remote-run rest V1/products -m GET -q "searchCriteria[pageSize]=10&fields=items[sku,name]"
22
+ $ mage-remote-run rest V1/products -m GET --page-size 10 --current-page 1
23
+ `)
24
+ .action(async (path, options) => {
25
+ try {
26
+ const client = await createClient();
27
+
28
+ // 1. Path
29
+ let requestPath = path;
30
+ if (!requestPath) {
31
+ requestPath = await input({
32
+ message: 'Enter the endpoint path (relative to base URL):',
33
+ validate: (value) => value.length > 0 ? true : 'Path is required'
34
+ });
35
+ }
36
+
37
+ // 2. Method
38
+ let method = options.method;
39
+ if (!method) {
40
+ method = await select({
41
+ message: 'Select HTTP Method:',
42
+ choices: [
43
+ { name: 'GET', value: 'GET' },
44
+ { name: 'POST', value: 'POST' },
45
+ { name: 'PUT', value: 'PUT' },
46
+ { name: 'DELETE', value: 'DELETE' }
47
+ ]
48
+ });
49
+ }
50
+ method = method.toUpperCase();
51
+
52
+ // 3. Data (Body)
53
+ let data = options.data;
54
+ const contentType = options.contentType || 'application/json';
55
+
56
+ if ((method === 'POST' || method === 'PUT') && data === undefined) {
57
+ data = await editor({
58
+ message: 'Enter request body:',
59
+ default: contentType === 'application/json' ? '{\n \n}' : ''
60
+ });
61
+ }
62
+
63
+ // Validate and Parse JSON
64
+ let parsedData = data;
65
+ if (contentType === 'application/json' && data) {
66
+ if (typeof data === 'string') {
67
+ try {
68
+ parsedData = JSON.parse(data);
69
+ } catch (e) {
70
+ throw new Error('Invalid JSON data provided.');
71
+ }
72
+ }
73
+ }
74
+
75
+ // 4. Execution
76
+ if (!options.format) {
77
+ console.log(chalk.gray(`Executing ${method} ${requestPath}...`));
78
+ }
79
+
80
+ const config = {
81
+ headers: {
82
+ 'Content-Type': contentType
83
+ }
84
+ };
85
+
86
+ if (options.format === 'json') {
87
+ config.headers['Accept'] = 'application/json';
88
+ } else if (options.format === 'xml') {
89
+ config.headers['Accept'] = 'application/xml';
90
+ }
91
+
92
+ // Parse query options
93
+ let params = {};
94
+ if (options.query) {
95
+ const searchParams = new URLSearchParams(options.query);
96
+ for (const [key, value] of searchParams) {
97
+ params[key] = value;
98
+ }
99
+ }
100
+
101
+ if (options.pageSize) {
102
+ params['searchCriteria[pageSize]'] = options.pageSize;
103
+ }
104
+
105
+ if (options.currentPage) {
106
+ params['searchCriteria[currentPage]'] = options.currentPage;
107
+ }
108
+
109
+ const response = await client.request(method, requestPath, parsedData, params, config);
110
+
111
+ // 5. Output
112
+ if (options.format === 'json') {
113
+ // Ensure we output valid JSON even if response is already an object
114
+ if (typeof response === 'object') {
115
+ console.log(JSON.stringify(response, null, 2));
116
+ } else {
117
+ // Attempt to parse if string, otherwise output as is (or error?)
118
+ // Usually response.data is parsed by axios if json.
119
+ console.log(response);
120
+ }
121
+ } else if (options.format === 'xml') {
122
+ console.log(response);
123
+ } else {
124
+ // Default behavior
125
+ if (typeof response === 'object') {
126
+ console.log(JSON.stringify(response, null, 2));
127
+ } else {
128
+ console.log(response);
129
+ }
130
+ }
131
+
132
+ } catch (e) {
133
+ handleError(e);
134
+ }
135
+ });
136
+ }
package/lib/prompts.js CHANGED
@@ -81,12 +81,22 @@ export async function askForProfileSettings(defaults = {}) {
81
81
  validate: value => (value || defaults.auth?.tokenSecret) ? true : 'Token Secret is required'
82
82
  });
83
83
 
84
+ const signatureMethod = await select({
85
+ message: 'Signature Method:',
86
+ default: defaults.auth?.signatureMethod || 'hmac-sha256',
87
+ choices: [
88
+ { name: 'HMAC-SHA256 (Default, Newer Versions)', value: 'hmac-sha256' },
89
+ { name: 'HMAC-SHA1 (Older Versions)', value: 'hmac-sha1' }
90
+ ]
91
+ });
92
+
84
93
  authConfig = {
85
94
  method: 'oauth1',
86
95
  consumerKey,
87
96
  consumerSecret: consumerSecret || defaults.auth?.consumerSecret,
88
97
  accessToken: accessToken || defaults.auth?.accessToken,
89
- tokenSecret: tokenSecret || defaults.auth?.tokenSecret
98
+ tokenSecret: tokenSecret || defaults.auth?.tokenSecret,
99
+ signatureMethod
90
100
  };
91
101
  }
92
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "0.24.0",
3
+ "version": "0.26.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": {