mage-remote-run 0.23.0 → 0.24.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 @@ const pkg = require('../package.json');
10
10
 
11
11
  const program = new Command();
12
12
 
13
+
13
14
  program
14
15
  .name('mage-remote-run')
15
16
  .description('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce')
@@ -48,12 +49,18 @@ import { startMcpServer } from '../lib/mcp.js';
48
49
  // But we need them registered early if we want them to show up in help even if config fails?
49
50
  // Actually registerCommands handles null profile by registering connection commands only.
50
51
 
51
- program.command('mcp')
52
+ program.command('mcp [args...]')
52
53
  .description('Run as MCP server')
53
54
  .option('--transport <type>', 'Transport type (stdio, http)', 'stdio')
54
55
  .option('--host <host>', 'HTTP Host', '127.0.0.1')
55
56
  .option('--port <port>', 'HTTP Port', '18098')
56
- .action(async (options) => {
57
+ .allowExcessArguments(true)
58
+ .allowUnknownOption(true)
59
+ .action(async (args, options) => {
60
+ // We ignore extra arguments but log them for debugging purposes
61
+ if (args && args.length > 0) {
62
+ // console.error(chalk.yellow(`[mage-remote-run] Warning: Received extra arguments for mcp command: ${args.join(' ')}`));
63
+ }
57
64
  await startMcpServer(options);
58
65
  });
59
66
 
@@ -68,7 +75,8 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
68
75
  const config = await loadConfig();
69
76
  if (config.showActiveConnectionHeader !== false) {
70
77
  const opts = actionCommand.opts();
71
- if (opts.format !== 'json' && opts.format !== 'xml') {
78
+ // Standard output corruption check: Don't print header if output is json/xml OR if running mcp command (which uses stdio)
79
+ if (opts.format !== 'json' && opts.format !== 'xml' && actionCommand.name() !== 'mcp') {
72
80
  console.log(chalk.cyan(`Active Connection: ${chalk.bold(profile.name)} (${profile.type})`));
73
81
  console.log(chalk.gray('━'.repeat(60)) + '\n');
74
82
  }
package/lib/b2b.js ADDED
@@ -0,0 +1,19 @@
1
+ export const B2B_MODULES = [
2
+ 'Magento_Company',
3
+ 'Magento_CompanyCredit',
4
+ 'Magento_CompanyPayment',
5
+ 'Magento_CompanyShipping',
6
+ 'Magento_NegotiableQuote',
7
+ 'Magento_PurchaseOrder',
8
+ 'Magento_RequisitionList',
9
+ 'Magento_SharedCatalog'
10
+ ];
11
+
12
+ export function getMissingB2BModules(modules = []) {
13
+ const installed = new Set(modules);
14
+ return B2B_MODULES.filter(moduleName => !installed.has(moduleName));
15
+ }
16
+
17
+ export function hasB2BModules(modules = []) {
18
+ return getMissingB2BModules(modules).length === 0;
19
+ }
@@ -57,14 +57,32 @@ const TYPE_MAPPINGS = {
57
57
  'ac-saas': [...GROUPS.CORE, ...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT] // Assuming SaaS has same feature set as PaaS for CLI
58
58
  };
59
59
 
60
+ const B2B_COMMANDS = new Set([registerCompanyCommands, registerPurchaseOrderCartCommands]);
61
+ const B2B_CHECK_TYPES = new Set(['ac-on-prem', 'ac-cloud-paas']);
62
+
63
+ function shouldRegisterCommandForProfile(registrar, profile) {
64
+ if (!B2B_COMMANDS.has(registrar)) {
65
+ return true;
66
+ }
67
+ if (!profile || !profile.type) {
68
+ return false;
69
+ }
70
+ if (!B2B_CHECK_TYPES.has(profile.type)) {
71
+ return true;
72
+ }
73
+ return profile.b2bModulesAvailable === true;
74
+ }
75
+
60
76
  export function registerCoreCommands(program) {
61
77
  // Backward compatibility: Register CORE group
62
78
  GROUPS.CORE.forEach(registrar => registrar(program));
63
79
  }
64
80
 
65
- export function registerCloudCommands(program) {
81
+ export function registerCloudCommands(program, profile = null) {
66
82
  // Backward compatibility
67
- [...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT].forEach(registrar => registrar(program));
83
+ [...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT]
84
+ .filter(registrar => shouldRegisterCommandForProfile(registrar, profile))
85
+ .forEach(registrar => registrar(program, profile));
68
86
  }
69
87
 
70
88
  // Deprecated: Use registerCommands instead
@@ -80,7 +98,9 @@ export const registerCommands = (program, profile) => {
80
98
  if (profile && profile.type) {
81
99
  const registrars = TYPE_MAPPINGS[profile.type];
82
100
  if (registrars) {
83
- registrars.forEach(registrar => registrar(program, profile));
101
+ registrars
102
+ .filter(registrar => shouldRegisterCommandForProfile(registrar, profile))
103
+ .forEach(registrar => registrar(program, profile));
84
104
  } else {
85
105
  // Fallback for unknown types
86
106
  GROUPS.CORE.forEach(registrar => registrar(program));
@@ -5,10 +5,12 @@ import { createClient } from '../api/factory.js';
5
5
  import { input, confirm, select } from '@inquirer/prompts';
6
6
  import inquirer from 'inquirer';
7
7
  import chalk from 'chalk';
8
+ import { getMissingB2BModules } from '../b2b.js';
8
9
 
9
10
  // Helper to handle interactive connection configuration and testing
10
11
  async function configureAndTestConnection(name, initialSettings = {}) {
11
12
  let settings = await askForProfileSettings(initialSettings);
13
+ let lastTestError = null;
12
14
 
13
15
  while (true) {
14
16
  const shouldTest = await confirm({
@@ -24,9 +26,11 @@ async function configureAndTestConnection(name, initialSettings = {}) {
24
26
  await client.get('V1/store/storeViews');
25
27
  const duration = Date.now() - start;
26
28
  console.log(chalk.green(`✔ Connection successful! (${duration}ms)`));
29
+ lastTestError = null;
27
30
  break; // Test passed, proceed to save
28
31
  } catch (e) {
29
32
  console.error(chalk.red(`✖ Connection failed: ${e.message}`));
33
+ lastTestError = e;
30
34
  const shouldEdit = await confirm({
31
35
  message: 'Connection failed. Do you want to change settings?',
32
36
  default: true
@@ -53,20 +57,160 @@ async function configureAndTestConnection(name, initialSettings = {}) {
53
57
  break;
54
58
  }
55
59
  }
60
+ settings = await updateProfileCapabilities(settings, lastTestError);
56
61
  return settings;
57
62
  }
58
63
 
64
+ function shouldCheckB2BModules(settings) {
65
+ return settings && ['ac-cloud-paas', 'ac-on-prem'].includes(settings.type);
66
+ }
67
+
68
+ function shouldCheckHyvaModules(settings) {
69
+ return settings && ['magento-os', 'mage-os', 'ac-on-prem', 'ac-cloud-paas'].includes(settings.type);
70
+ }
71
+
72
+ async function updateProfileCapabilities(settings, lastTestError) {
73
+ if (settings && settings.type === 'ac-saas') {
74
+ settings.b2bModulesAvailable = true;
75
+ settings.hyvaCommerceAvailable = false;
76
+ settings.hyvaThemeAvailable = false;
77
+ return settings;
78
+ }
79
+
80
+ if (!shouldCheckB2BModules(settings) && !shouldCheckHyvaModules(settings)) {
81
+ if (settings && 'b2bModulesAvailable' in settings) delete settings.b2bModulesAvailable;
82
+ if (settings && 'hyvaCommerceAvailable' in settings) delete settings.hyvaCommerceAvailable;
83
+ if (settings && 'hyvaThemeAvailable' in settings) delete settings.hyvaThemeAvailable;
84
+ if (settings && 'hyvaModulesAvailable' in settings) delete settings.hyvaModulesAvailable; // Cleanup legacy
85
+ return settings;
86
+ }
87
+
88
+ try {
89
+ const client = await createClient(settings);
90
+ const data = await client.get('V1/modules');
91
+ const modules = Array.isArray(data) ? data : (Array.isArray(data?.items) ? data.items : []);
92
+
93
+ if (shouldCheckB2BModules(settings)) {
94
+ const missing = getMissingB2BModules(modules);
95
+ settings.b2bModulesAvailable = missing.length === 0;
96
+ if (process.env.DEBUG) {
97
+ if (settings.b2bModulesAvailable) {
98
+ console.log(chalk.gray('DEBUG: B2B modules detected.'));
99
+ } else {
100
+ console.log(chalk.gray(`DEBUG: Missing B2B modules: ${missing.join(', ')}`));
101
+ }
102
+ }
103
+ }
104
+
105
+ if (shouldCheckHyvaModules(settings)) {
106
+ settings.hyvaCommerceAvailable = modules.includes('Hyva_Commerce');
107
+ settings.hyvaThemeAvailable = modules.includes('Hyva_Theme');
108
+
109
+ // Cleanup old property if exists
110
+ if ('hyvaModulesAvailable' in settings) delete settings.hyvaModulesAvailable;
111
+
112
+ if (process.env.DEBUG) {
113
+ if (settings.hyvaCommerceAvailable) console.log(chalk.gray('DEBUG: Hyvä Commerce detected.'));
114
+ if (settings.hyvaThemeAvailable) console.log(chalk.gray('DEBUG: Hyvä Theme detected.'));
115
+ }
116
+ }
117
+
118
+ } catch (e) {
119
+ if (process.env.DEBUG) {
120
+ const suffix = lastTestError ? ' (connection test failed)' : '';
121
+ console.log(chalk.gray(`DEBUG: Unable to detect modules${suffix}: ${e.message}`));
122
+ }
123
+ if (shouldCheckB2BModules(settings)) settings.b2bModulesAvailable = null;
124
+ if (shouldCheckHyvaModules(settings)) {
125
+ settings.hyvaCommerceAvailable = null;
126
+ settings.hyvaThemeAvailable = null;
127
+ }
128
+ }
129
+
130
+ return settings;
131
+ }
132
+
133
+ async function ensureProfileCapabilities(profileName, profile, config) {
134
+ if (!profile || !profile.type) {
135
+ return false;
136
+ }
137
+
138
+ let updated = false;
139
+
140
+ // Migrate old property if exists
141
+ if ('hyvaModulesAvailable' in profile) {
142
+ profile.hyvaCommerceAvailable = profile.hyvaModulesAvailable;
143
+ delete profile.hyvaModulesAvailable;
144
+ updated = true;
145
+ }
146
+
147
+ if (profile.type === 'ac-saas') {
148
+ if (profile.b2bModulesAvailable !== true) {
149
+ profile.b2bModulesAvailable = true;
150
+ updated = true;
151
+ }
152
+ if (profile.hyvaCommerceAvailable !== false) {
153
+ profile.hyvaCommerceAvailable = false;
154
+ updated = true;
155
+ }
156
+ if (profile.hyvaThemeAvailable !== false) {
157
+ profile.hyvaThemeAvailable = false;
158
+ updated = true;
159
+ }
160
+ if (updated) config.profiles[profileName] = profile;
161
+ return updated;
162
+ }
163
+
164
+ const checkB2B = shouldCheckB2BModules(profile);
165
+ const checkHyva = shouldCheckHyvaModules(profile);
166
+
167
+ if (!checkB2B && !checkHyva) return false;
168
+
169
+ // Check if we need to detect
170
+ const needsB2B = checkB2B && profile.b2bModulesAvailable === undefined;
171
+ const needsHyva = checkHyva && (profile.hyvaCommerceAvailable === undefined || profile.hyvaThemeAvailable === undefined);
172
+
173
+ if (needsB2B || needsHyva) {
174
+ const newSettings = await updateProfileCapabilities({ ...profile });
175
+
176
+ if (checkB2B) {
177
+ profile.b2bModulesAvailable = newSettings.b2bModulesAvailable;
178
+ }
179
+ if (checkHyva) {
180
+ profile.hyvaCommerceAvailable = newSettings.hyvaCommerceAvailable;
181
+ profile.hyvaThemeAvailable = newSettings.hyvaThemeAvailable;
182
+ }
183
+
184
+ config.profiles[profileName] = profile;
185
+ return true;
186
+ }
187
+
188
+ if (updated) config.profiles[profileName] = profile;
189
+ return updated;
190
+ }
59
191
 
60
192
  // Helper to print connection status
61
- async function printConnectionStatus(config) {
193
+ async function printConnectionStatus(config, options = {}) {
62
194
  if (!config.activeProfile) {
63
- console.log(chalk.yellow('No active profile configured. Run "connection add" or "connection select".'));
195
+ if (options.format === 'json') {
196
+ console.log(JSON.stringify({ error: 'No active profile configured' }));
197
+ } else {
198
+ console.log(chalk.yellow('No active profile configured. Run "connection add" or "connection select".'));
199
+ }
64
200
  return;
65
201
  }
66
202
 
67
203
  const profile = config.profiles[config.activeProfile];
68
204
 
69
205
  if (profile) {
206
+ if (options.format === 'json') {
207
+ console.log(JSON.stringify({
208
+ activeProfile: config.activeProfile,
209
+ ...profile
210
+ }, null, 2));
211
+ return;
212
+ }
213
+
70
214
  // ASCII Logos
71
215
  const logos = {
72
216
  adobe: chalk.red(`
@@ -149,9 +293,43 @@ async function printConnectionStatus(config) {
149
293
  console.log(logo);
150
294
  }
151
295
 
152
- console.log(chalk.bold('Active Profile:'), chalk.green(config.activeProfile));
153
- console.log(`Type: ${profile.type}`);
154
- console.log(`URL: ${profile.url}`);
296
+ const rows = [
297
+ ['Active Profile', chalk.green(config.activeProfile)],
298
+ ['Type', profile.type],
299
+ ['URL', profile.url]
300
+ ];
301
+
302
+ if (['ac-cloud-paas', 'ac-on-prem', 'ac-saas'].includes(profile.type)) {
303
+ let b2bStatus = '?';
304
+ if (profile.type === 'ac-saas') {
305
+ b2bStatus = chalk.green('Yes');
306
+ } else if (profile.b2bModulesAvailable === true) {
307
+ b2bStatus = chalk.green('Yes');
308
+ } else if (profile.b2bModulesAvailable === false) {
309
+ b2bStatus = chalk.yellow('No');
310
+ }
311
+ rows.push(['B2B Modules', b2bStatus]);
312
+ }
313
+
314
+ if (['magento-os', 'mage-os', 'ac-on-prem', 'ac-cloud-paas'].includes(profile.type)) {
315
+ let hyvaCommerceStatus = '?';
316
+ if (profile.hyvaCommerceAvailable === true) {
317
+ hyvaCommerceStatus = chalk.green('Yes');
318
+ } else if (profile.hyvaCommerceAvailable === false) {
319
+ hyvaCommerceStatus = chalk.yellow('No');
320
+ }
321
+ rows.push(['Hyvä Commerce', hyvaCommerceStatus]);
322
+
323
+ let hyvaThemeStatus = '?';
324
+ if (profile.hyvaThemeAvailable === true) {
325
+ hyvaThemeStatus = chalk.green('Yes');
326
+ } else if (profile.hyvaThemeAvailable === false) {
327
+ hyvaThemeStatus = chalk.yellow('No');
328
+ }
329
+ rows.push(['Hyvä Theme', hyvaThemeStatus]);
330
+ }
331
+
332
+ printTable(['Configuration', 'Value'], rows);
155
333
  } else {
156
334
  console.log(chalk.red('Profile not found in configuration!'));
157
335
  }
@@ -217,20 +395,117 @@ Examples:
217
395
  //-------------------------------------------------------
218
396
  connections.command('list')
219
397
  .description('List connection profiles')
398
+ .option('--format <format>', 'Output format (table, json, csv)', 'table')
220
399
  .addHelpText('after', `
221
400
  Examples:
222
401
  $ mage-remote-run connection list
402
+ $ mage-remote-run connection list --format json
403
+ $ mage-remote-run connection list --format csv
223
404
  `)
224
- .action(async () => {
405
+ .action(async (options) => {
225
406
  try {
226
407
  const config = await loadConfig();
408
+ let updated = false;
409
+ for (const [name, profile] of Object.entries(config.profiles || {})) {
410
+ updated = (await ensureProfileCapabilities(name, profile, config)) || updated;
411
+ }
412
+ if (updated) {
413
+ await saveConfig(config);
414
+ }
415
+
416
+ if (options.format === 'json') {
417
+ console.log(JSON.stringify(config.profiles || {}, null, 2));
418
+ return;
419
+ }
420
+
421
+ if (options.format === 'csv') {
422
+ const { stringify } = await import('csv-stringify/sync');
423
+ const rows = Object.entries(config.profiles || {}).map(([name, p]) => ({
424
+ name,
425
+ type: p.type,
426
+ url: p.url,
427
+ b2b_modules_available: p.b2bModulesAvailable,
428
+ hyva_theme_available: p.hyvaThemeAvailable,
429
+ hyva_commerce_available: p.hyvaCommerceAvailable,
430
+ active: name === config.activeProfile
431
+ }));
432
+ console.log(stringify(rows, { header: true }));
433
+ return;
434
+ }
435
+
227
436
  const rows = Object.entries(config.profiles || {}).map(([name, p]) => [
228
437
  name,
229
438
  p.type,
230
439
  p.url,
440
+ ['ac-cloud-paas', 'ac-on-prem', 'ac-saas'].includes(p.type)
441
+ ? (p.type === 'ac-saas'
442
+ ? 'Yes'
443
+ : (p.b2bModulesAvailable === true ? 'Yes' : (p.b2bModulesAvailable === false ? 'No' : '?')))
444
+ : '',
445
+ (p.hyvaThemeAvailable === true ? 'Yes' : (p.hyvaThemeAvailable === false ? 'No' : '?')),
446
+ (p.hyvaCommerceAvailable === true ? 'Yes' : (p.hyvaCommerceAvailable === false ? 'No' : '?')),
231
447
  name === config.activeProfile ? chalk.green('Yes') : 'No'
232
448
  ]);
233
- printTable(['Name', 'Type', 'URL', 'Active'], rows);
449
+
450
+ const headers = ['Name', 'Type', 'URL', 'B2B', 'Hyvä Theme', 'Hyvä Comm.', 'Active'];
451
+ const termWidth = process.stdout.columns;
452
+
453
+ if (termWidth) {
454
+ const visibleLength = (str) => {
455
+ return ('' + str).replace(/\u001b\[[0-9;]*m/g, '').length;
456
+ };
457
+
458
+ const colWidths = headers.map((h, i) => {
459
+ return Math.max(h.length, ...rows.map(r => visibleLength(r[i])));
460
+ });
461
+
462
+ const overhead = (headers.length * 3) + 1;
463
+ const totalWidth = colWidths.reduce((a, b) => a + b, 0) + overhead;
464
+
465
+ if (totalWidth > termWidth) {
466
+ const available = termWidth - overhead;
467
+ // Indices: 0=Name, 1=Type, 2=URL, 3=B2B, 4=HT, 5=HC, 6=Active
468
+ const fixedIndices = [1, 3, 4, 5, 6];
469
+ const fixedWidth = fixedIndices.reduce((sum, i) => sum + colWidths[i], 0);
470
+
471
+ let fluidWidth = available - fixedWidth;
472
+ if (fluidWidth < 20) fluidWidth = 20;
473
+
474
+ const nameWidth = colWidths[0];
475
+ const urlWidth = colWidths[2];
476
+ let targetNameWidth = nameWidth;
477
+ let targetUrlWidth = urlWidth;
478
+
479
+ // Try to preserve Name, sacrifice URL
480
+ if (nameWidth + urlWidth > fluidWidth) {
481
+ // Keep Name as is if possible (assuming min URL width of 20)
482
+ if (nameWidth + 20 <= fluidWidth) {
483
+ targetUrlWidth = fluidWidth - nameWidth;
484
+ targetNameWidth = nameWidth;
485
+ } else {
486
+ // We have to cut Name too
487
+ // Give Name 30% or min 15
488
+ let w = Math.floor(fluidWidth * 0.3);
489
+ if (w < 15) w = 15;
490
+ targetNameWidth = Math.min(nameWidth, w);
491
+ targetUrlWidth = fluidWidth - targetNameWidth;
492
+ }
493
+ }
494
+
495
+ const truncate = (str, len) => {
496
+ if (!str) return str;
497
+ if (visibleLength(str) <= len) return str;
498
+ return str.substring(0, len - 3) + '...';
499
+ };
500
+
501
+ rows.forEach(row => {
502
+ row[0] = truncate(row[0], targetNameWidth);
503
+ row[2] = truncate(row[2], targetUrlWidth);
504
+ });
505
+ }
506
+ }
507
+
508
+ printTable(headers, rows);
234
509
  } catch (e) { handleError(e); }
235
510
  });
236
511
 
@@ -401,14 +676,21 @@ Examples:
401
676
  //-------------------------------------------------------
402
677
  connections.command('status')
403
678
  .description('Show current configuration status')
679
+ .option('--format <format>', 'Output format (text, json)', 'text')
404
680
  .addHelpText('after', `
405
681
  Examples:
406
682
  $ mage-remote-run connection status
683
+ $ mage-remote-run connection status --format json
407
684
  `)
408
- .action(async () => {
685
+ .action(async (options) => {
409
686
  try {
410
687
  const config = await loadConfig();
411
- await printConnectionStatus(config);
688
+ const activeProfileName = config.activeProfile;
689
+ const activeProfile = activeProfileName ? config.profiles[activeProfileName] : null;
690
+ if (activeProfile && await ensureProfileCapabilities(activeProfileName, activeProfile, config)) {
691
+ await saveConfig(config);
692
+ }
693
+ await printConnectionStatus(config, options);
412
694
  } catch (e) { handleError(e); }
413
695
  });
414
696
 
package/lib/mcp.js CHANGED
@@ -62,8 +62,8 @@ export async function startMcpServer(options) {
62
62
 
63
63
  } else {
64
64
  // STDIO
65
- console.error(`Protocol: stdio`);
66
- console.error(`Registered Tools: ${toolsCount}`);
65
+ console.error(`Protocol: stdio`);
66
+ console.error(`Registered Tools: ${toolsCount}`);
67
67
  const transport = new StdioServerTransport();
68
68
  await server.connect(transport);
69
69
  }
@@ -82,7 +82,7 @@ function registerTools(server, program) {
82
82
  }
83
83
 
84
84
  // It's a leaf command, register as tool
85
- // Tool name: Replace spaces/colons with underscores.
85
+ // Tool name: Replace spaces/colons with underscores.
86
86
  // Example: website list -> website_list
87
87
  const toolName = cmdName.replace(/[^a-zA-Z0-9_]/g, '_');
88
88
 
@@ -105,7 +105,7 @@ function registerTools(server, program) {
105
105
 
106
106
  cmd.options.forEach(opt => {
107
107
  const name = opt.name(); // e.g. "format" for --format
108
- // Check flags to guess type.
108
+ // Check flags to guess type.
109
109
  // -f, --format <type> -> string
110
110
  // -v, --verbose -> boolean
111
111
 
@@ -186,9 +186,9 @@ async function executeCommand(cmdDefinition, args, parentName) {
186
186
  // Wait, registerTools calls: `executeCommand(cmd, args, parentName)`
187
187
  // If parentName is "website", and cmd is "list", we need "website list"
188
188
 
189
- // NOTE: parentName in processCommand is built recursively with underscores?
190
- // In processCommand(subCmd, cmdName), cmdName is "parent_sub".
191
- // So if we have website -> list.
189
+ // NOTE: parentName in processCommand is built recursively with underscores?
190
+ // In processCommand(subCmd, cmdName), cmdName is "parent_sub".
191
+ // So if we have website -> list.
192
192
  // processCommand(website) -> processCommand(list, "website")
193
193
  // parentName in executeCommand is "website".
194
194
  // cmd.name() is "list".
@@ -212,7 +212,7 @@ async function executeCommand(cmdDefinition, args, parentName) {
212
212
  // `cmdName` = `parentName_cmd.name()`.
213
213
  // So for `website list`:
214
214
  // `processCommand(website, '')` -> `cmdName="website"`.
215
- // -> `processCommand(list, "website")`.
215
+ // -> `processCommand(list, "website")`.
216
216
  // -> register tool "website_list". `parentName` passed to execute is "website".
217
217
 
218
218
  // So `parentName` is the accumulated prefix with underscores.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "0.23.0",
3
+ "version": "0.24.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": {
@@ -39,6 +39,7 @@
39
39
  "cli-table3": "^0.6.5",
40
40
  "commander": "^14.0.2",
41
41
  "csv-parse": "^6.1.0",
42
+ "csv-stringify": "^6.6.0",
42
43
  "env-paths": "^3.0.0",
43
44
  "html-to-text": "^9.0.5",
44
45
  "inquirer": "^13.1.0",