mage-remote-run 1.3.1 → 1.4.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
- import { loadConfig } from '../lib/config.js';
4
+ import { loadConfig, saveConfig } from '../lib/config.js';
5
5
  import chalk from 'chalk';
6
6
 
7
7
  import { createRequire } from 'module';
@@ -78,20 +78,23 @@ const profile = await getActiveProfile();
78
78
  // but we can pass a way to get it or just pass null for now if not used at startup.
79
79
  // Also mcpServer is not running here unless mcp command is used.
80
80
  const appContext = {
81
- program,
82
- config: await loadConfig(), // Re-load or reuse config
83
- profile,
84
- eventBus,
85
- events,
86
- createClient
81
+ program,
82
+ config: await loadConfig(), // Re-load or reuse config
83
+ saveConfig,
84
+ profile,
85
+ eventBus,
86
+ events,
87
+ createClient
87
88
  };
88
89
 
89
90
  const pluginLoader = new PluginLoader(appContext);
90
91
  await pluginLoader.loadPlugins();
91
92
 
92
93
  eventBus.emit(events.INIT, appContext);
94
+ import { registerVirtualCommands } from '../lib/commands/virtual.js';
93
95
 
94
96
  registerCommands(program, profile);
97
+ registerVirtualCommands(program, appContext.config, profile);
95
98
 
96
99
  program.hook('preAction', async (thisCommand, actionCommand) => {
97
100
  eventBus.emit(events.BEFORE_COMMAND, { thisCommand, actionCommand, profile });
@@ -113,7 +116,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
113
116
  });
114
117
 
115
118
  program.hook('postAction', async (thisCommand, actionCommand) => {
116
- eventBus.emit(events.AFTER_COMMAND, { thisCommand, actionCommand, profile });
119
+ eventBus.emit(events.AFTER_COMMAND, { thisCommand, actionCommand, profile });
117
120
  });
118
121
 
119
122
  import { expandCommandAbbreviations } from '../lib/command-helper.js';
@@ -32,6 +32,7 @@ Examples:
32
32
  $ mage-remote-run order list --fields "increment_id,grand_total,customer_email"
33
33
  $ mage-remote-run order list --filter "grand_total>=100" --add-fields "base_grand_total,billing_address.city"
34
34
  $ mage-remote-run order list --filter "status=pending" --filter "grand_total>100"
35
+ $ mage-remote-run order list --filter "created_at>=2024-01-01"
35
36
  $ mage-remote-run order list --sort "grand_total:DESC" --sort "created_at:ASC"
36
37
  $ mage-remote-run order list --format json
37
38
  `);
@@ -25,6 +25,7 @@ Examples:
25
25
  $ mage-remote-run product list --sort-by price --sort-order DESC
26
26
  $ mage-remote-run product list --sort "price:DESC" "sku:ASC"
27
27
  $ mage-remote-run product list --filter "type_id=simple" "price>=100"
28
+ $ mage-remote-run product list --filter "created_at>=2024-01-01"
28
29
  $ mage-remote-run product list --fields "sku,name,price"
29
30
  $ mage-remote-run product list --add-fields "created_at,updated_at"
30
31
  `);
@@ -0,0 +1,295 @@
1
+ import { createClient } from '../api/factory.js';
2
+ import { handleError, formatOutput, addFormatOption, addFilterOption, addSortOption, addPaginationOptions, buildSearchCriteria, buildSortCriteria } from '../utils.js';
3
+ import chalk from 'chalk';
4
+
5
+ function getInputDefinitions(cmdDef) {
6
+ const definitions = new Map();
7
+ const sources = [
8
+ ['parameter', cmdDef.parameter],
9
+ ['parameter', cmdDef.parameters],
10
+ ['option', cmdDef.option],
11
+ ['option', cmdDef.options]
12
+ ];
13
+
14
+ for (const [source, group] of sources) {
15
+ if (!group || typeof group !== 'object') {
16
+ continue;
17
+ }
18
+
19
+ for (const [key, definition] of Object.entries(group)) {
20
+ definitions.set(key, {
21
+ ...definition,
22
+ _source: source
23
+ });
24
+ }
25
+ }
26
+
27
+ return Object.fromEntries(definitions);
28
+ }
29
+
30
+ function buildOptionFlags(key, definition = {}) {
31
+ if (definition.flags) {
32
+ return definition.flags;
33
+ }
34
+
35
+ const shortFlag = definition.short
36
+ ? `${definition.short.startsWith('-') ? definition.short : `-${definition.short}`}, `
37
+ : '';
38
+ const longFlag = definition.long
39
+ ? (definition.long.startsWith('-') ? definition.long : `--${definition.long}`)
40
+ : `--${key}`;
41
+
42
+ if (definition.type === 'boolean') {
43
+ return `${shortFlag}${longFlag}`;
44
+ }
45
+
46
+ const argName = definition.argName || 'value';
47
+ const valuePlaceholder = definition.variadic ? `<${argName}...>` : `<${argName}>`;
48
+ return `${shortFlag}${longFlag} ${valuePlaceholder}`;
49
+ }
50
+
51
+ function getFlagTokens(flags) {
52
+ return flags
53
+ .split(',')
54
+ .map(flag => flag.trim().split(' ')[0])
55
+ .filter(Boolean);
56
+ }
57
+
58
+ function hasOption(command, flags) {
59
+ const requestedFlags = new Set(getFlagTokens(flags));
60
+
61
+ return (command.options || []).some(option => {
62
+ const existingFlags = [option.short, option.long, ...(option.flags ? getFlagTokens(option.flags) : [])]
63
+ .filter(Boolean);
64
+
65
+ return existingFlags.some(flag => requestedFlags.has(flag));
66
+ });
67
+ }
68
+
69
+ function addCommandOption(command, flags, description, defaultValue, required = false) {
70
+ if (hasOption(command, flags)) {
71
+ return command;
72
+ }
73
+
74
+ if (required) {
75
+ if (defaultValue !== undefined) {
76
+ return command.requiredOption(flags, description, defaultValue);
77
+ }
78
+
79
+ return command.requiredOption(flags, description);
80
+ }
81
+
82
+ if (defaultValue !== undefined) {
83
+ return command.option(flags, description, defaultValue);
84
+ }
85
+
86
+ return command.option(flags, description);
87
+ }
88
+
89
+ function normalizeListValue(value) {
90
+ if (value === undefined || value === null) {
91
+ return [];
92
+ }
93
+
94
+ if (Array.isArray(value)) {
95
+ return value.filter(item => typeof item === 'string' && item.trim().length > 0);
96
+ }
97
+
98
+ if (typeof value === 'string' && value.trim().length > 0) {
99
+ return [value];
100
+ }
101
+
102
+ return [];
103
+ }
104
+
105
+ function interpolateTemplateValue(template, values) {
106
+ if (typeof template !== 'string' || template.length === 0) {
107
+ return template;
108
+ }
109
+
110
+ return template
111
+ .replace(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
112
+ const value = values[key];
113
+ return value === undefined || value === null ? match : String(value);
114
+ })
115
+ .replace(/\{:\s*([a-zA-Z0-9_]+)\s*\}/g, (match, key) => {
116
+ const value = values[key];
117
+ return value === undefined || value === null ? match : String(value);
118
+ })
119
+ .replace(/:([a-zA-Z0-9_]+)/g, (match, key) => {
120
+ const value = values[key];
121
+ return value === undefined || value === null ? match : String(value);
122
+ });
123
+ }
124
+
125
+ export function registerVirtualCommands(program, config, profile) {
126
+ if (!config || !config.commands || !Array.isArray(config.commands)) {
127
+ return;
128
+ }
129
+
130
+ for (const cmdDef of config.commands) {
131
+ // Validation namespace vs command
132
+ if (!cmdDef.name) {
133
+ if (process.env.DEBUG) {
134
+ console.error(chalk.yellow(`Skipping invalid virtual command definition without name: ${JSON.stringify(cmdDef)}`));
135
+ }
136
+ continue;
137
+ }
138
+
139
+ // Filtering by connection_types
140
+ if (cmdDef.connection_types && Array.isArray(cmdDef.connection_types)) {
141
+ if (!profile || !profile.type || !cmdDef.connection_types.includes(profile.type)) {
142
+ continue; // Skip if profile type is not in connection_types
143
+ }
144
+ }
145
+
146
+ // Command Generation: Find or create subcommands
147
+ let currentCmd = program;
148
+ const parts = cmdDef.name.split(' ');
149
+
150
+ for (let i = 0; i < parts.length; i++) {
151
+ const part = parts[i];
152
+ const existing = currentCmd.commands.find(c => c.name() === part);
153
+ if (existing) {
154
+ currentCmd = existing;
155
+ } else {
156
+ currentCmd = currentCmd.command(part);
157
+ }
158
+ }
159
+
160
+ if (cmdDef.description) {
161
+ currentCmd.description(cmdDef.description);
162
+ } else if (cmdDef.endpoint && cmdDef.method) {
163
+ currentCmd.description(`Virtual command to execute ${cmdDef.method.toUpperCase()} ${cmdDef.endpoint}`);
164
+ }
165
+
166
+ if (cmdDef.summary) {
167
+ currentCmd.summary(cmdDef.summary);
168
+ } else if (cmdDef.description) {
169
+ // Fallback to description if summary is not explicitly provided
170
+ // We use a shortened version if it's too long
171
+ const summaryText = cmdDef.description.split('\n')[0];
172
+ currentCmd.summary(summaryText.length > 80 ? summaryText.substring(0, 77) + '...' : summaryText);
173
+ } else if (cmdDef.endpoint && cmdDef.method) {
174
+ currentCmd.summary(`${cmdDef.method.toUpperCase()} ${cmdDef.endpoint}`);
175
+ }
176
+
177
+ // It is just a namespace wrapper if there's no endpoint or method
178
+ if (!cmdDef.endpoint || !cmdDef.method) {
179
+ continue;
180
+ }
181
+
182
+ if (!hasOption(currentCmd, '-f, --format <type>')) {
183
+ addFormatOption(currentCmd);
184
+ }
185
+
186
+ // Map parameters to options
187
+ const inputDefinitions = getInputDefinitions(cmdDef);
188
+ const parameterKeys = Object.keys(inputDefinitions);
189
+ for (const key of parameterKeys) {
190
+ const paramDef = inputDefinitions[key];
191
+ const flags = buildOptionFlags(key, paramDef);
192
+ const desc = paramDef.description || `${paramDef._source === 'option' ? 'Option' : 'Parameter'} ${key}`;
193
+
194
+ addCommandOption(currentCmd, flags, desc, paramDef.default, paramDef.required);
195
+ }
196
+
197
+ // Add filter and sort options if enabled
198
+ if (cmdDef.supports_filters !== false) {
199
+ if (!hasOption(currentCmd, '--filter <filters...>')) {
200
+ addFilterOption(currentCmd);
201
+ }
202
+ if (!hasOption(currentCmd, '--sort-by <field>')) {
203
+ addSortOption(currentCmd);
204
+ }
205
+ if (!hasOption(currentCmd, '-p, --page <number>')) {
206
+ addPaginationOptions(currentCmd);
207
+ }
208
+ }
209
+
210
+ // Add action
211
+ currentCmd.action(async (options) => {
212
+ try {
213
+ const client = await createClient();
214
+ const method = cmdDef.method.toUpperCase();
215
+ let requestPath = cmdDef.endpoint;
216
+
217
+ const payload = {};
218
+ let queryParams = {};
219
+
220
+ // Process options based on whether they are in the endpoint path
221
+ for (const key of parameterKeys) {
222
+ const value = options[key];
223
+ if (value === undefined || value === null) {
224
+ continue;
225
+ }
226
+
227
+ const placeholder = `:${key}`;
228
+ if (requestPath.includes(placeholder)) {
229
+ requestPath = requestPath.replace(placeholder, encodeURIComponent(value));
230
+ } else {
231
+ if (['POST', 'PUT', 'PATCH'].includes(method)) {
232
+ // Can be mapped to nested objects if needed, but for now simple flat payload
233
+ payload[key] = value;
234
+ } else {
235
+ queryParams[key] = value;
236
+ }
237
+ }
238
+ }
239
+
240
+ if (cmdDef.supports_filters !== false) {
241
+ const predefinedFilters = normalizeListValue(cmdDef.filters ?? cmdDef.filter)
242
+ .map(filter => interpolateTemplateValue(filter, options));
243
+ const mergedFilterOptions = {
244
+ ...options,
245
+ filter: [
246
+ ...predefinedFilters,
247
+ ...normalizeListValue(options.filter)
248
+ ]
249
+ };
250
+ const { params: searchParams } = buildSearchCriteria(mergedFilterOptions);
251
+ const { params: sortParams } = buildSortCriteria(options);
252
+
253
+ queryParams = {
254
+ ...queryParams,
255
+ ...searchParams,
256
+ ...sortParams
257
+ };
258
+ }
259
+
260
+ // Clean up any remaining path placeholders if they had defaults or were optional
261
+ requestPath = requestPath.replace(/:[a-zA-Z0-9_]+/g, '');
262
+
263
+ const reqConfig = {
264
+ headers: {
265
+ 'Content-Type': 'application/json'
266
+ }
267
+ };
268
+
269
+ if (options.format === 'json') reqConfig.headers.Accept = 'application/json';
270
+ else if (options.format === 'xml') reqConfig.headers.Accept = 'application/xml';
271
+
272
+ const response = await client.request(
273
+ method,
274
+ requestPath,
275
+ Object.keys(payload).length > 0 ? payload : undefined,
276
+ Object.keys(queryParams).length > 0 ? queryParams : undefined,
277
+ reqConfig
278
+ );
279
+
280
+ if (formatOutput(options, response)) {
281
+ return;
282
+ }
283
+
284
+ if (typeof response === 'object') {
285
+ console.log(JSON.stringify(response, null, 2));
286
+ } else {
287
+ console.log(response);
288
+ }
289
+
290
+ } catch (error) {
291
+ handleError(error);
292
+ }
293
+ });
294
+ }
295
+ }
package/lib/mcp.js CHANGED
@@ -158,6 +158,10 @@ export async function startMcpServer(options) {
158
158
  let token = options.token || process.env.MAGE_REMOTE_RUN_MCP_TOKEN;
159
159
 
160
160
  if (!token) {
161
+ if (!process.stderr.isTTY) {
162
+ throw new Error('Authentication token is required for HTTP transport in non-TTY environments. Please provide it via --token or MAGE_REMOTE_RUN_MCP_TOKEN environment variable.');
163
+ }
164
+
161
165
  token = crypto.randomBytes(16).toString('hex');
162
166
  console.error(chalk.yellow(`--------------------------------------------------------------------------------`));
163
167
  console.error(chalk.yellow(`MCP Server Authentication Token: `) + chalk.green.bold(token));
@@ -41,28 +41,34 @@ export class PluginLoader {
41
41
  // 2. Try global node_modules (npm)
42
42
  try {
43
43
  const globalNpmPath = path.join(globalDirs.npm.packages, pluginName);
44
- if (fs.existsSync(globalNpmPath)) {
44
+ const npmExists = await fs.promises.access(globalNpmPath).then(() => true).catch(() => false);
45
+ if (npmExists) {
45
46
  const pkgJsonPath = path.join(globalNpmPath, 'package.json');
46
- if (fs.existsSync(pkgJsonPath)) {
47
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
47
+ const pkgJsonExists = await fs.promises.access(pkgJsonPath).then(() => true).catch(() => false);
48
+ if (pkgJsonExists) {
49
+ const pkgContent = await fs.promises.readFile(pkgJsonPath, 'utf-8');
50
+ const pkg = JSON.parse(pkgContent);
48
51
  const mainFile = pkg.main || 'index.js';
49
52
  pluginPath = path.join(globalNpmPath, mainFile);
50
53
  } else {
51
- pluginPath = path.join(globalNpmPath, 'index.js');
54
+ pluginPath = path.join(globalNpmPath, 'index.js');
52
55
  }
53
56
  } else {
54
57
  // 3. Try global node_modules (yarn)
55
- const globalYarnPath = path.join(globalDirs.yarn.packages, pluginName);
56
- if (fs.existsSync(globalYarnPath)) {
58
+ const globalYarnPath = path.join(globalDirs.yarn.packages, pluginName);
59
+ const yarnExists = await fs.promises.access(globalYarnPath).then(() => true).catch(() => false);
60
+ if (yarnExists) {
57
61
  const pkgJsonPath = path.join(globalYarnPath, 'package.json');
58
- if (fs.existsSync(pkgJsonPath)) {
59
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
62
+ const pkgJsonExists = await fs.promises.access(pkgJsonPath).then(() => true).catch(() => false);
63
+ if (pkgJsonExists) {
64
+ const pkgContent = await fs.promises.readFile(pkgJsonPath, 'utf-8');
65
+ const pkg = JSON.parse(pkgContent);
60
66
  const mainFile = pkg.main || 'index.js';
61
67
  pluginPath = path.join(globalYarnPath, mainFile);
62
68
  } else {
63
- pluginPath = path.join(globalYarnPath, 'index.js');
69
+ pluginPath = path.join(globalYarnPath, 'index.js');
64
70
  }
65
- }
71
+ }
66
72
  }
67
73
  } catch (globalErr) {
68
74
  // Ignore global errors, proceed to throw if not found
@@ -81,18 +87,79 @@ export class PluginLoader {
81
87
  console.log(chalk.gray(`Loading plugin from: ${pluginUrl}`));
82
88
  }
83
89
 
84
- const pluginModule = await import(pluginUrl);
90
+ // Find plugin root to load static config
91
+ let currentDir = path.dirname(pluginPath);
92
+ let pluginRoot = currentDir;
93
+ while (currentDir !== path.parse(currentDir).root) {
94
+ const pkgPath = path.join(currentDir, 'package.json');
95
+ const pkgExists = await fs.promises.access(pkgPath).then(() => true).catch(() => false);
96
+ if (pkgExists) {
97
+ pluginRoot = currentDir;
98
+ break;
99
+ }
100
+ currentDir = path.dirname(currentDir);
101
+ }
102
+
103
+ // Try to load static configuration
104
+ let staticConfig = null;
105
+ const mageConfigPath = path.join(pluginRoot, 'mage-remote-run.json');
106
+ const pkgPath = path.join(pluginRoot, 'package.json');
107
+
108
+ if (await fs.promises.access(mageConfigPath).then(() => true).catch(() => false)) {
109
+ staticConfig = JSON.parse(await fs.promises.readFile(mageConfigPath, 'utf8'));
110
+ } else if (await fs.promises.access(pkgPath).then(() => true).catch(() => false)) {
111
+ const pkg = JSON.parse(await fs.promises.readFile(pkgPath, 'utf8'));
112
+ if (pkg['mage-remote-run']) {
113
+ staticConfig = pkg['mage-remote-run'];
114
+ }
115
+ }
116
+
117
+ if (staticConfig) {
118
+ if (process.env.DEBUG) {
119
+ console.log(chalk.gray(`Found static configuration for plugin: ${pluginName}`));
120
+ }
121
+ // Merge static config into appContext.config
122
+ if (staticConfig.commands && Array.isArray(staticConfig.commands)) {
123
+ if (!this.appContext.config.commands) {
124
+ this.appContext.config.commands = [];
125
+ }
126
+ this.appContext.config.commands.push(...staticConfig.commands);
127
+ }
128
+ }
129
+
130
+ let pluginModule = null;
131
+ try {
132
+ pluginModule = await import(pluginUrl);
133
+ } catch (err) {
134
+ if (process.env.DEBUG) {
135
+ console.error(chalk.yellow(`Could not import plugin module for ${pluginName} at ${pluginUrl}: ${err.message}`));
136
+ }
137
+ }
85
138
 
86
139
  if (pluginModule && pluginModule.default) {
87
140
  if (typeof pluginModule.default === 'function') {
88
141
  await pluginModule.default(this.appContext);
89
142
  this.plugins.push({ name: pluginName, module: pluginModule });
90
143
  if (process.env.DEBUG) {
91
- console.log(chalk.gray(`Loaded plugin: ${pluginName}`));
144
+ console.log(chalk.gray(`Loaded plugin script: ${pluginName}`));
92
145
  }
93
146
  } else {
94
- console.warn(chalk.yellow(`Plugin ${pluginName} does not export a default function.`));
147
+ if (!staticConfig) {
148
+ console.warn(chalk.yellow(`Plugin ${pluginName} does not export a default function and has no static config.`));
149
+ } else {
150
+ this.plugins.push({ name: pluginName, module: pluginModule });
151
+ if (process.env.DEBUG) {
152
+ console.log(chalk.gray(`Loaded plugin config only: ${pluginName}`));
153
+ }
154
+ }
95
155
  }
156
+ } else if (staticConfig) {
157
+ this.plugins.push({ name: pluginName, module: null });
158
+ if (process.env.DEBUG) {
159
+ console.log(chalk.gray(`Loaded plugin config only: ${pluginName}`));
160
+ }
161
+ } else {
162
+ console.warn(chalk.yellow(`Plugin ${pluginName} could not be loaded because it has no default export and no static config.`));
96
163
  }
97
164
  }
98
165
  }
package/lib/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { input, select, password, confirm } from '@inquirer/prompts';
1
+ import { input, select, password } from '@inquirer/prompts';
2
2
 
3
3
  export async function askForProfileSettings(defaults = {}) {
4
4
  const type = await select({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-remote-run",
3
- "version": "1.3.1",
3
+ "version": "1.4.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": {