mage-remote-run 0.19.0 → 0.20.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.
@@ -39,14 +39,14 @@ program
39
39
 
40
40
 
41
41
  import {
42
- registerConnectionCommands,
43
- registerCoreCommands,
44
- registerCloudCommands
42
+ registerCommands
45
43
  } from '../lib/command-registry.js';
46
44
  import { getActiveProfile } from '../lib/config.js';
47
45
  import { startMcpServer } from '../lib/mcp.js';
48
46
 
49
- registerConnectionCommands(program);
47
+ // Connection commands are registered dynamically via registerCommands
48
+ // But we need them registered early if we want them to show up in help even if config fails?
49
+ // Actually registerCommands handles null profile by registering connection commands only.
50
50
 
51
51
  program.command('mcp')
52
52
  .description('Run as MCP server')
@@ -58,14 +58,7 @@ program.command('mcp')
58
58
  });
59
59
 
60
60
  const profile = await getActiveProfile();
61
-
62
- if (profile) {
63
- registerCoreCommands(program);
64
-
65
- if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
66
- registerCloudCommands(program);
67
- }
68
- }
61
+ registerCommands(program, profile);
69
62
 
70
63
  program.hook('preAction', async (thisCommand, actionCommand) => {
71
64
  // Check if we have an active profile and if format is not json/xml
@@ -8,63 +8,74 @@ import { registerProductsCommands } from './commands/products.js';
8
8
  import { registerCompanyCommands } from './commands/company.js';
9
9
  import { registerTaxCommands } from './commands/tax.js';
10
10
  import { registerInventoryCommands } from './commands/inventory.js';
11
- import { registerAdobeIoEventsCommands } from './commands/adobe-io-events.js';
11
+ import { registerEventsCommands } from './commands/events.js';
12
12
  import { registerWebhooksCommands } from './commands/webhooks.js';
13
13
  import { registerPurchaseOrderCartCommands } from './commands/purchase-order-cart.js';
14
+ import { registerImportCommands } from './commands/import.js';
14
15
  import { registerConsoleCommand } from './commands/console.js';
15
16
 
16
17
  export { registerConnectionCommands, registerConsoleCommand };
17
18
 
19
+ const GROUPS = {
20
+ CORE: [
21
+ registerWebsitesCommands,
22
+ registerStoresCommands,
23
+ registerCustomersCommands,
24
+ registerOrdersCommands,
25
+ registerEavCommands,
26
+ registerProductsCommands,
27
+ registerTaxCommands,
28
+ registerInventoryCommands,
29
+ registerConsoleCommand
30
+ ],
31
+ COMMERCE: [
32
+ registerCompanyCommands,
33
+ registerPurchaseOrderCartCommands
34
+ ],
35
+ CLOUD: [
36
+ registerEventsCommands,
37
+ registerWebhooksCommands
38
+ ],
39
+ IMPORT: [
40
+ registerImportCommands
41
+ ]
42
+ };
43
+
44
+ const TYPE_MAPPINGS = {
45
+ 'magento-os': [...GROUPS.CORE],
46
+ 'mage-os': [...GROUPS.CORE],
47
+ 'ac-on-prem': [...GROUPS.CORE, ...GROUPS.COMMERCE, ...GROUPS.IMPORT],
48
+ 'ac-cloud-paas': [...GROUPS.CORE, ...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT],
49
+ 'ac-saas': [...GROUPS.CORE, ...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT] // Assuming SaaS has same feature set as PaaS for CLI
50
+ };
51
+
18
52
  export function registerCoreCommands(program) {
19
- registerWebsitesCommands(program);
20
- registerStoresCommands(program);
21
- registerCustomersCommands(program);
22
- registerOrdersCommands(program);
23
- registerEavCommands(program);
24
- registerProductsCommands(program);
25
- registerTaxCommands(program);
26
- registerInventoryCommands(program);
27
- registerConsoleCommand(program);
53
+ // Backward compatibility: Register CORE group
54
+ GROUPS.CORE.forEach(registrar => registrar(program));
28
55
  }
29
56
 
30
57
  export function registerCloudCommands(program) {
31
- registerAdobeIoEventsCommands(program);
32
- registerCompanyCommands(program);
33
- registerPurchaseOrderCartCommands(program);
34
- registerWebhooksCommands(program);
58
+ // Backward compatibility
59
+ [...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT].forEach(registrar => registrar(program));
35
60
  }
36
61
 
62
+ // Deprecated: Use registerCommands instead
37
63
  export function registerAllCommands(program) {
38
64
  registerConnectionCommands(program);
39
- registerCoreCommands(program);
40
- registerCloudCommands(program);
65
+ const all = new Set([...GROUPS.CORE, ...GROUPS.COMMERCE, ...GROUPS.CLOUD, ...GROUPS.IMPORT]);
66
+ all.forEach(registrar => registrar(program));
41
67
  }
42
68
 
43
- // Alias for backwards compatibility with console.js (and potential other usages)
44
69
  export const registerCommands = (program, profile) => {
45
- // Note: console.js passes profile, but registerAllCommands doesn't strictly use it
46
- // (it registers everything, profile checks happen inside or via selective registration).
47
- // The original behavior in console.js:
48
- // const profile = await getActiveProfile();
49
- // registerCommands(localProgram, profile);
50
-
51
- // In bin/mage-remote-run.js old logic:
52
- // registerConnectionCommands(program);
53
- // if (profile) { registerWebsitesCommands... }
54
-
55
- // We should replicate that 'selective' registration if we want to match exact behavior?
56
- // BUT console.js wants to register ALL available commands for the profile.
57
-
58
70
  registerConnectionCommands(program);
59
71
 
60
- // Simple logic: If profile exists, register all.
61
- // console.js usage implies profile is present if it's running (or it tries to get it).
62
-
63
- if (profile) {
64
- registerCoreCommands(program);
65
- // Cloud check
66
- if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
67
- registerCloudCommands(program);
72
+ if (profile && profile.type) {
73
+ const registrars = TYPE_MAPPINGS[profile.type];
74
+ if (registrars) {
75
+ registrars.forEach(registrar => registrar(program, profile));
76
+ } else {
77
+ // Fallback for unknown types
78
+ GROUPS.CORE.forEach(registrar => registrar(program));
68
79
  }
69
80
  }
70
81
  };
@@ -0,0 +1,277 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { printTable, handleError } from '../utils.js';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+
6
+ export function registerEventsCommands(program) {
7
+ const events = program.command('event').description('Manage Adobe I/O Events');
8
+
9
+ //-------------------------------------------------------
10
+ // "event check-configuration" Command (Renamed from adobe-io-event)
11
+ //-------------------------------------------------------
12
+ events.command('check-configuration')
13
+ .description('Check Adobe I/O Event configuration')
14
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
15
+ .addHelpText('after', `
16
+ Examples:
17
+ $ mage-remote-run event check-configuration
18
+ $ mage-remote-run event check-configuration --format json
19
+ `)
20
+ .action(async (options) => {
21
+ try {
22
+ const client = await createClient();
23
+ const headers = {};
24
+ if (options.format === 'json') headers['Accept'] = 'application/json';
25
+ else if (options.format === 'xml') headers['Accept'] = 'application/xml';
26
+
27
+ const data = await client.get('V1/adobe_io_events/check_configuration', {}, { headers });
28
+
29
+ if (options.format === 'json') {
30
+ console.log(JSON.stringify(data, null, 2));
31
+ return;
32
+ }
33
+ if (options.format === 'xml') {
34
+ console.log(data);
35
+ return;
36
+ }
37
+
38
+ console.log(chalk.bold.blue('\nšŸ” Configuration Check Result'));
39
+ console.log(chalk.gray('━'.repeat(60)));
40
+
41
+ if (typeof data === 'object' && data !== null) {
42
+ if (Array.isArray(data)) {
43
+ console.log(JSON.stringify(data, null, 2));
44
+ } else {
45
+ Object.entries(data).forEach(([key, value]) => {
46
+ const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
47
+ let displayValue = value;
48
+ if (typeof value === 'boolean') {
49
+ displayValue = value ? chalk.green('Yes') : chalk.red('No');
50
+ } else if (value === null) {
51
+ displayValue = chalk.gray('null');
52
+ } else if (typeof value === 'object') {
53
+ displayValue = JSON.stringify(value);
54
+ }
55
+ console.log(` ${chalk.bold(label + ':').padEnd(35)} ${displayValue}`);
56
+ });
57
+ }
58
+ } else {
59
+ console.log(` ${data}`);
60
+ }
61
+ console.log('');
62
+ } catch (e) { handleError(e); }
63
+ });
64
+
65
+ const provider = events.command('provider').description('Manage event providers');
66
+
67
+ //-------------------------------------------------------
68
+ // "event provider list" Command
69
+ //-------------------------------------------------------
70
+ provider.command('list')
71
+ .description('List event providers')
72
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
73
+ .addHelpText('after', `
74
+ Examples:
75
+ $ mage-remote-run event provider list
76
+ `)
77
+ .action(async (options) => {
78
+ try {
79
+ const client = await createClient();
80
+ const headers = {};
81
+ if (options.format === 'json') headers['Accept'] = 'application/json';
82
+
83
+ const data = await client.get('V1/eventing/eventProvider', {}, { headers });
84
+
85
+ if (options.format === 'json') {
86
+ console.log(JSON.stringify(data, null, 2));
87
+ return;
88
+ }
89
+
90
+ const rows = (data || []).map(p => [p.id, p.label, p.description]);
91
+ printTable(['ID', 'Label', 'Description'], rows);
92
+ } catch (e) { handleError(e); }
93
+ });
94
+
95
+ //-------------------------------------------------------
96
+ // "event provider show" Command
97
+ //-------------------------------------------------------
98
+ provider.command('show <id>')
99
+ .description('Show event provider details')
100
+ .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
101
+ .action(async (id, options) => {
102
+ try {
103
+ const client = await createClient();
104
+ const headers = {};
105
+ if (options.format === 'json') headers['Accept'] = 'application/json';
106
+
107
+ // We try fetching all and filtering because the user mapped "show" to the specific endpoint that looks like a collection
108
+ const data = await client.get('V1/eventing/eventProvider', {}, { headers });
109
+
110
+ let item;
111
+ if (Array.isArray(data)) {
112
+ item = data.find(p => p.id == id);
113
+ } else if (data.id == id) {
114
+ item = data;
115
+ }
116
+
117
+ if (!item) {
118
+ throw new Error(`Event Provider '${id}' not found.`);
119
+ }
120
+
121
+ if (options.format === 'json') {
122
+ console.log(JSON.stringify(item, null, 2));
123
+ return;
124
+ }
125
+
126
+ console.log(chalk.bold.blue('\nšŸ” Event Provider Details'));
127
+ console.log(chalk.gray('━'.repeat(60)));
128
+ Object.entries(item).forEach(([key, value]) => {
129
+ console.log(` ${chalk.bold(key + ':').padEnd(20)} ${value}`);
130
+ });
131
+ console.log('');
132
+
133
+ } catch (e) { handleError(e); }
134
+ });
135
+
136
+ //-------------------------------------------------------
137
+ // "event provider create" Command
138
+ //-------------------------------------------------------
139
+ provider.command('create')
140
+ .description('Create a new event provider')
141
+ .action(async () => {
142
+ try {
143
+ const client = await createClient();
144
+
145
+ // Attempt to auto-detect instance_id from profile settings (SaaS only)
146
+ let defaultInstanceId = undefined;
147
+ try {
148
+ console.log(chalk.gray('Attempting to auto-detect Instance ID...'));
149
+ const { getActiveProfile } = await import('../config.js');
150
+ const profile = await getActiveProfile();
151
+
152
+ if (profile && (profile.type === 'ac-saas' || profile.type === 'saas')) {
153
+ try {
154
+ // Extract last part of URL path
155
+ const url = new URL(profile.url);
156
+ const pathParts = url.pathname.split('/').filter(p => p.length > 0);
157
+ if (pathParts.length > 0) {
158
+ defaultInstanceId = pathParts[pathParts.length - 1];
159
+ console.log(chalk.gray(`Found Instance ID (from URL): ${defaultInstanceId}`));
160
+ } else {
161
+ console.log(chalk.gray('Could not extract Instance ID from URL.'));
162
+ }
163
+ } catch (e) {
164
+ console.log(chalk.gray('Invalid URL in profile configuration.'));
165
+ }
166
+ } else {
167
+ console.log(chalk.gray('Instance ID detection only supported for SaaS connections.'));
168
+ }
169
+ } catch (e) {
170
+ console.log(chalk.gray('Could not auto-detect Instance ID.'));
171
+ }
172
+
173
+ const answers = await inquirer.prompt([
174
+ {
175
+ type: 'input',
176
+ name: 'provider_id',
177
+ message: `Enter Provider ID ${chalk.dim('(Found in Adobe Developer Console > Project > Events > Event Provider > ID)')}:`,
178
+ validate: input => input ? true : 'Provider ID is required'
179
+ },
180
+ {
181
+ type: 'input',
182
+ name: 'label',
183
+ message: `Enter Provider Label ${chalk.dim('(A friendly name for this provider)')}:`,
184
+ validate: input => input ? true : 'Label is required'
185
+ },
186
+ {
187
+ type: 'input',
188
+ name: 'description',
189
+ message: `Enter Provider Description ${chalk.dim('(Optional)')}:`
190
+ },
191
+ {
192
+ type: 'input',
193
+ name: 'instance_id',
194
+ message: `Enter Instance ID ${chalk.dim('(Found in "check-configuration" or Adobe Developer Console)')}:`,
195
+ default: defaultInstanceId
196
+ },
197
+ {
198
+ type: 'editor',
199
+ name: 'workspace_configuration',
200
+ message: `Enter Workspace Configuration ${chalk.dim('(JSON from Adobe Developer Console > Download Project Config)')}:`
201
+ }
202
+ ]);
203
+
204
+ const payload = {
205
+ eventProvider: {
206
+ provider_id: answers.provider_id,
207
+ instance_id: answers.instance_id,
208
+ label: answers.label,
209
+ description: answers.description,
210
+ workspace_configuration: answers.workspace_configuration
211
+ }
212
+ };
213
+
214
+ const result = await client.post('V1/eventing/eventProvider', payload);
215
+ console.log(chalk.green('\nāœ… Event Provider Created Successfully'));
216
+ console.log(JSON.stringify(result, null, 2));
217
+
218
+ } catch (e) { handleError(e); }
219
+ });
220
+
221
+ //-------------------------------------------------------
222
+ // "event provider delete" Command
223
+ //-------------------------------------------------------
224
+ provider.command('delete <id>')
225
+ .description('Delete an event provider')
226
+ .action(async (id) => {
227
+ try {
228
+ const client = await createClient();
229
+ // Assumed endpoint based on standard REST, though prompt said DELETE /V1/eventing/eventProvider
230
+ // Usually DELETE requires an ID. I will append the ID.
231
+ // Wait, if the prompt says DELETE /V1/eventing/eventProvider, it MIGHT expect a body or query param?
232
+ // Standard Magento: DELETE /V1/eventing/eventProvider/:id
233
+ // I will try appending ID.
234
+ await client.delete(`V1/eventing/eventProvider/${id}`);
235
+ console.log(chalk.green(`\nāœ… Event Provider '${id}' deleted successfully.`));
236
+ } catch (e) { handleError(e); }
237
+ });
238
+
239
+ //-------------------------------------------------------
240
+ // "event supported-list" Command
241
+ //-------------------------------------------------------
242
+ events.command('supported-list')
243
+ .description('List supported events')
244
+ .option('-f, --format <type>', 'Output format (text, json)', 'text')
245
+ .action(async (options) => {
246
+ try {
247
+ const client = await createClient();
248
+ const headers = {};
249
+ if (options.format === 'json') headers['Accept'] = 'application/json';
250
+
251
+ const data = await client.get('V1/eventing/supportedList', {}, { headers });
252
+
253
+ if (options.format === 'json') {
254
+ console.log(JSON.stringify(data, null, 2));
255
+ return;
256
+ }
257
+
258
+ // Assuming data is list of strings or objects.
259
+ // If objects, we might need to know structure.
260
+ // Prompt didn't specify structure. Assuming list of strings or objects with 'name'/'id'.
261
+ if (Array.isArray(data)) {
262
+ if (data.length > 0 && typeof data[0] === 'string') {
263
+ data.forEach(d => console.log(d));
264
+ } else {
265
+ // Fallback table
266
+ const keys = Object.keys(data[0] || {});
267
+ const rows = data.map(d => Object.values(d));
268
+ printTable(keys, rows);
269
+ }
270
+ } else {
271
+ console.log(data);
272
+ }
273
+
274
+ } catch (e) { handleError(e); }
275
+ });
276
+
277
+ }
@@ -0,0 +1,275 @@
1
+
2
+ import { createClient } from '../api/factory.js';
3
+ import { handleError, readInput, validateAdobeCommerce, validatePaaSOrOnPrem } from '../utils.js';
4
+ import chalk from 'chalk';
5
+ import fs from 'fs';
6
+ import { input, select, confirm, editor } from '@inquirer/prompts';
7
+ import search from '@inquirer/search';
8
+ import os from 'os';
9
+
10
+ async function getImportBehaviorAndEntity(options) {
11
+ let entityType = options.entityType;
12
+ if (!entityType) {
13
+ const ENTITY_TYPES = [
14
+ { name: 'Advanced Pricing', value: 'advanced_pricing' },
15
+ { name: 'Products', value: 'catalog_product' },
16
+ { name: 'Customers and Addresses (single file)', value: 'customer_composite' },
17
+ { name: 'Customers Main File', value: 'customer' },
18
+ { name: 'Customer Addresses', value: 'customer_address' },
19
+ { name: 'Customer Finances', value: 'customer_finance' },
20
+ { name: 'Stock Sources', value: 'stock_sources' },
21
+ { name: 'Custom...', value: 'custom' }
22
+ ];
23
+
24
+ entityType = await search({
25
+ message: 'Select Entity Type:',
26
+ source: async (term) => {
27
+ if (!term) {
28
+ return ENTITY_TYPES;
29
+ }
30
+ return ENTITY_TYPES.filter((type) =>
31
+ type.name.toLowerCase().includes(term.toLowerCase()) ||
32
+ type.value.toLowerCase().includes(term.toLowerCase())
33
+ );
34
+ }
35
+ });
36
+
37
+ if (entityType === 'custom') {
38
+ entityType = await input({
39
+ message: 'Enter Custom Entity Type:',
40
+ validate: value => value ? true : 'Entity Type is required'
41
+ });
42
+ }
43
+ }
44
+
45
+ let behavior = options.behavior;
46
+ if (!behavior) {
47
+ behavior = await select({
48
+ message: 'Import Behavior:',
49
+ choices: [
50
+ { name: 'Append/Update', value: 'append' },
51
+ { name: 'Replace', value: 'replace' },
52
+ { name: 'Delete', value: 'delete_entity' }
53
+ ],
54
+ default: 'append'
55
+ });
56
+ }
57
+
58
+ return { entityType, behavior };
59
+ }
60
+
61
+ async function getValidationOptions(options) {
62
+ let validationStrategy = options.validationStrategy;
63
+ if (!validationStrategy) {
64
+ validationStrategy = await select({
65
+ message: 'Validation Strategy:',
66
+ choices: [
67
+ { name: 'Stop on Error', value: 'validation-stop-on-errors' },
68
+ { name: 'Skip error entries', value: 'validation-skip-errors' }
69
+ ],
70
+ default: 'validation-stop-on-errors'
71
+ });
72
+ }
73
+
74
+ let allowedErrorCount = options.allowedErrorCount;
75
+ if (!allowedErrorCount) {
76
+ allowedErrorCount = await input({
77
+ message: 'Allowed Error Count:',
78
+ default: '10',
79
+ validate: (value) => {
80
+ const num = parseInt(value, 10);
81
+ return !isNaN(num) && num >= 0 ? true : 'Please enter a valid number';
82
+ }
83
+ });
84
+ }
85
+
86
+ return {
87
+ validationStrategy,
88
+ allowedErrorCount: parseInt(allowedErrorCount, 10)
89
+ };
90
+ }
91
+
92
+ export function registerImportCommands(program, profile) {
93
+ const importCmd = program.command('import').description('Import data');
94
+
95
+ //-------------------------------------------------------
96
+ // "import json" Command
97
+ //-------------------------------------------------------
98
+ importCmd.command('json [file]')
99
+ .description('Import data from JSON (Adobe Commerce)')
100
+ .option('--entity-type <type>', 'Entity Type (e.g. catalog_product, customer)')
101
+ .option('--behavior <behavior>', 'Import Behavior (append, replace, delete_entity)')
102
+ .option('--validation-strategy <strategy>', 'Validation Strategy (validation-stop-on-errors, validation-skip-errors)')
103
+ .option('--allowed-error-count <count>', 'Allowed Error Count')
104
+ .addHelpText('after', `
105
+ Examples:
106
+ Product from file:
107
+ $ mage-remote-run import json products.json --entity-type=catalog_product --allowed-error-count=10 --validation-strategy=validation-skip-errors
108
+
109
+ From STDIN (piped):
110
+ $ cat data.json | mage-remote-run import json
111
+ `)
112
+ .action(async (file, options) => {
113
+ try {
114
+ await validateAdobeCommerce();
115
+
116
+ let content = await readInput(file);
117
+ if (!content) {
118
+ const fromFile = await confirm({
119
+ message: 'Do you want to import a file?',
120
+ default: true
121
+ });
122
+
123
+ if (fromFile) {
124
+ const filePath = await input({
125
+ message: 'Path to JSON file:',
126
+ validate: value => {
127
+ let p = value;
128
+ if (p.startsWith('~/') || p === '~') p = p.replace(/^~/, os.homedir());
129
+ return fs.existsSync(p) ? true : 'File not found';
130
+ }
131
+ });
132
+ content = await readInput(filePath);
133
+ } else {
134
+ content = await editor({
135
+ message: 'Enter JSON content',
136
+ postfix: '.json'
137
+ });
138
+ }
139
+ }
140
+
141
+ let data;
142
+ try {
143
+ data = JSON.parse(content);
144
+ } catch (e) {
145
+ throw new Error('Invalid JSON data');
146
+ }
147
+
148
+ const { entityType, behavior } = await getImportBehaviorAndEntity(options);
149
+ const { validationStrategy, allowedErrorCount } = await getValidationOptions(options);
150
+
151
+ const payload = {
152
+ source: {
153
+ entity: entityType,
154
+ behavior: behavior,
155
+ validation_strategy: validationStrategy,
156
+ allowed_error_count: allowedErrorCount,
157
+ items: Array.isArray(data) ? data : [data]
158
+ }
159
+ };
160
+
161
+ const client = await createClient();
162
+ const endpoint = 'V1/import/json';
163
+
164
+ try {
165
+ const result = await client.post(endpoint, payload);
166
+ console.log(chalk.green('Import submitted successfully.'));
167
+ if (result) console.log(JSON.stringify(result, null, 2));
168
+ } catch (apiError) {
169
+ if (apiError.message && (apiError.message.includes('Route') || apiError.message.includes('404'))) {
170
+ throw new Error(`The endpoint "${endpoint}" was not found on the server.\nEnsure your Magento instance has the required Import API module installed.`);
171
+ }
172
+ throw apiError;
173
+ }
174
+
175
+ } catch (e) {
176
+ if (e.name === 'ExitPromptError') return;
177
+ handleError(e);
178
+ }
179
+ });
180
+
181
+ //-------------------------------------------------------
182
+ // "import csv" Command
183
+ //-------------------------------------------------------
184
+ // Only register/show if profile is allowed (PaaS or On-Prem)
185
+ if (profile && (profile.type === 'ac-cloud-paas' || profile.type === 'ac-on-prem')) {
186
+ importCmd.command('csv [file]')
187
+ .description('Import data from CSV (Adobe Commerce PaaS/On-Prem)')
188
+ .option('--entity-type <type>', 'Entity Type (e.g. catalog_product, customer)')
189
+ .option('--behavior <behavior>', 'Import Behavior (append, replace, delete_entity)')
190
+ .option('--validation-strategy <strategy>', 'Validation Strategy (validation-stop-on-errors, validation-skip-errors)')
191
+ .option('--allowed-error-count <count>', 'Allowed Error Count')
192
+ .option('--field-separator <char>', 'Field Separator', ',')
193
+ .option('--multi-value-separator <char>', 'Multiple Value Separator', ',')
194
+ .option('--empty-value-constant <string>', 'Empty Attribute Value Constant', '__EMPTY__VALUE__')
195
+ .option('--images-file-dir <dir>', 'Images File Directory', 'var/import/images')
196
+ .addHelpText('after', `
197
+ Examples:
198
+ Interactive:
199
+ $ mage-remote-run import csv products.csv
200
+
201
+ Non-Interactive:
202
+ $ mage-remote-run import csv products.csv --entity-type catalog_product --behavior append
203
+
204
+ Custom Separators:
205
+ $ mage-remote-run import csv products.csv --field-separator ";" --multi-value-separator "|"
206
+ `)
207
+ .action(async (file, options) => {
208
+ try {
209
+ await validatePaaSOrOnPrem();
210
+
211
+ let content = await readInput(file);
212
+ if (!content) {
213
+ const fromFile = await confirm({
214
+ message: 'Do you want to import a file?',
215
+ default: true
216
+ });
217
+
218
+ if (fromFile) {
219
+ const filePath = await input({
220
+ message: 'Path to CSV file:',
221
+ validate: value => {
222
+ let p = value;
223
+ if (p.startsWith('~/') || p === '~') p = p.replace(/^~/, os.homedir());
224
+ return fs.existsSync(p) ? true : 'File not found';
225
+ }
226
+ });
227
+ content = await readInput(filePath);
228
+ } else {
229
+ content = await editor({
230
+ message: 'Enter CSV content',
231
+ postfix: '.csv'
232
+ });
233
+ }
234
+ }
235
+
236
+ console.log(chalk.blue(`Size: ${content.length} characters.`));
237
+
238
+ const { entityType, behavior } = await getImportBehaviorAndEntity(options);
239
+ const { validationStrategy, allowedErrorCount } = await getValidationOptions(options);
240
+
241
+ const payload = {
242
+ source: {
243
+ entity: entityType,
244
+ behavior: behavior,
245
+ csv_data: Buffer.from(content).toString('base64'),
246
+ import_field_separator: options.fieldSeparator,
247
+ import_multiple_value_separator: options.multiValueSeparator,
248
+ import_empty_attribute_value_constant: options.emptyValueConstant,
249
+ import_images_file_dir: options.imagesFileDir,
250
+ validation_strategy: validationStrategy,
251
+ allowed_error_count: allowedErrorCount
252
+ }
253
+ };
254
+
255
+ const client = await createClient();
256
+ const endpoint = 'V1/import/csv';
257
+
258
+ try {
259
+ const result = await client.post(endpoint, payload);
260
+ console.log(chalk.green('Import submitted successfully.'));
261
+ if (result) console.log(JSON.stringify(result, null, 2));
262
+ } catch (apiError) {
263
+ if (apiError.message && (apiError.message.includes('Route') || apiError.message.includes('404'))) {
264
+ throw new Error(`The endpoint "${endpoint}" was not found on the server.\nEnsure your Magento instance has the required Import API module installed.`);
265
+ }
266
+ throw apiError;
267
+ }
268
+
269
+ } catch (e) {
270
+ if (e.name === 'ExitPromptError') return;
271
+ handleError(e);
272
+ }
273
+ });
274
+ }
275
+ }
package/lib/utils.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import Table from 'cli-table3';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import { loadConfig } from './config.js';
3
6
 
4
7
  export function printTable(headers, data) {
5
8
  const table = new Table({
@@ -42,3 +45,44 @@ export function handleError(error) {
42
45
  console.error(error);
43
46
  }
44
47
  }
48
+
49
+ export async function readInput(filePath) {
50
+ if (filePath) {
51
+ if (filePath.startsWith('~/') || filePath === '~') {
52
+ filePath = filePath.replace(/^~/, os.homedir());
53
+ }
54
+
55
+ if (!fs.existsSync(filePath)) {
56
+ throw new Error(`File not found: ${filePath}`);
57
+ }
58
+ return fs.readFileSync(filePath, 'utf8');
59
+ }
60
+
61
+ if (!process.stdin.isTTY) {
62
+ let data = '';
63
+ for await (const chunk of process.stdin) {
64
+ data += chunk;
65
+ }
66
+ return data;
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ export async function validateAdobeCommerce() {
73
+ const config = await loadConfig();
74
+ const profile = config.profiles[config.activeProfile];
75
+ const allowed = ['ac-cloud-paas', 'ac-saas', 'ac-on-prem'];
76
+ if (!profile || !allowed.includes(profile.type)) {
77
+ throw new Error('This command is only available for Adobe Commerce (Cloud, SaaS, On-Premise).');
78
+ }
79
+ }
80
+
81
+ export async function validatePaaSOrOnPrem() {
82
+ const config = await loadConfig();
83
+ const profile = config.profiles[config.activeProfile];
84
+ const allowed = ['ac-cloud-paas', 'ac-on-prem'];
85
+ if (!profile || !allowed.includes(profile.type)) {
86
+ throw new Error('This command is only available for Adobe Commerce (Cloud/On-Premise).');
87
+ }
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "0.19.0",
3
+ "version": "0.20.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": {
@@ -38,6 +38,7 @@
38
38
  "chalk": "^5.6.2",
39
39
  "cli-table3": "^0.6.5",
40
40
  "commander": "^14.0.2",
41
+ "csv-parse": "^6.1.0",
41
42
  "env-paths": "^3.0.0",
42
43
  "html-to-text": "^9.0.5",
43
44
  "inquirer": "^13.1.0",
@@ -1,70 +0,0 @@
1
- import { createClient } from '../api/factory.js';
2
- import { printTable, handleError } from '../utils.js';
3
- import chalk from 'chalk';
4
-
5
- export function registerAdobeIoEventsCommands(program) {
6
- const adobeIoEvents = program.command('adobe-io-event').description('Manage Adobe I/O Events');
7
-
8
-
9
- //-------------------------------------------------------
10
- // "adobe-io-event check-configuration" Command
11
- //-------------------------------------------------------
12
- adobeIoEvents.command('check-configuration')
13
- .description('Check Adobe I/O Event configuration')
14
- .option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
15
- .addHelpText('after', `
16
- Examples:
17
- $ mage-remote-run adobe-io-event check-configuration
18
- $ mage-remote-run adobe-io-event check-configuration --format json
19
- `)
20
- .action(async (options) => {
21
- try {
22
- const client = await createClient();
23
- const headers = {};
24
- if (options.format === 'json') headers['Accept'] = 'application/json';
25
- else if (options.format === 'xml') headers['Accept'] = 'application/xml';
26
-
27
- // The endpoint is likely returning a simple boolean/string or a small object.
28
- // Based on standard Magento APIs, let's assume it returns a boolean or object.
29
- // We'll inspect the data structure.
30
- const data = await client.get('V1/adobe_io_events/check_configuration', {}, { headers });
31
-
32
- if (options.format === 'json') {
33
- console.log(JSON.stringify(data, null, 2));
34
- return;
35
- }
36
- if (options.format === 'xml') {
37
- console.log(data);
38
- return;
39
- }
40
-
41
- console.log(chalk.bold.blue('\nšŸ” Configuration Check Result'));
42
- console.log(chalk.gray('━'.repeat(60)));
43
-
44
- if (typeof data === 'object' && data !== null) {
45
- if (Array.isArray(data)) {
46
- console.log(JSON.stringify(data, null, 2));
47
- } else {
48
- Object.entries(data).forEach(([key, value]) => {
49
- const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
50
-
51
- let displayValue = value;
52
- if (typeof value === 'boolean') {
53
- displayValue = value ? chalk.green('Yes') : chalk.red('No');
54
- } else if (value === null) {
55
- displayValue = chalk.gray('null');
56
- } else if (typeof value === 'object') {
57
- displayValue = JSON.stringify(value);
58
- }
59
-
60
- console.log(` ${chalk.bold(label + ':').padEnd(35)} ${displayValue}`);
61
- });
62
- }
63
- } else {
64
- console.log(` ${data}`);
65
- }
66
- console.log('');
67
-
68
- } catch (e) { handleError(e); }
69
- });
70
- }