mage-remote-run 0.19.0 ā 0.21.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/bin/mage-remote-run.js +5 -12
- package/lib/command-registry.js +49 -38
- package/lib/commands/events.js +277 -0
- package/lib/commands/import.js +275 -0
- package/lib/utils.js +44 -0
- package/package.json +2 -1
- package/lib/commands/adobe-io-events.js +0 -70
package/bin/mage-remote-run.js
CHANGED
|
@@ -39,14 +39,14 @@ program
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
import {
|
|
42
|
-
|
|
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
|
-
|
|
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
|
package/lib/command-registry.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
3
|
+
"version": "0.21.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
|
-
}
|