mage-remote-run 0.13.0 → 0.15.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 +72 -50
- package/lib/command-registry.js +53 -20
- package/lib/commands/connections.js +7 -1
- package/lib/commands/console.js +114 -83
- package/lib/mcp.js +271 -0
- package/package.json +4 -2
package/bin/mage-remote-run.js
CHANGED
|
@@ -11,24 +11,24 @@ const pkg = require('../package.json');
|
|
|
11
11
|
const program = new Command();
|
|
12
12
|
|
|
13
13
|
program
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
.name('mage-remote-run')
|
|
15
|
+
.description('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce')
|
|
16
|
+
.version(pkg.version)
|
|
17
|
+
.configureHelp({
|
|
18
|
+
visibleCommands: (cmd) => {
|
|
19
|
+
const commands = cmd.commands.filter(c => !c._hidden);
|
|
20
|
+
return commands.sort((a, b) => {
|
|
21
|
+
if (a.name() === 'connection') return -1;
|
|
22
|
+
if (b.name() === 'connection') return 1;
|
|
23
|
+
return a.name().localeCompare(b.name());
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
subcommandTerm: (cmd) => chalk.cyan(cmd.name()),
|
|
27
|
+
subcommandDescription: (cmd) => chalk.gray(cmd.description()),
|
|
28
|
+
optionTerm: (option) => chalk.yellow(option.flags),
|
|
29
|
+
optionDescription: (option) => chalk.gray(option.description)
|
|
30
|
+
})
|
|
31
|
+
.addHelpText('before', chalk.hex('#FFA500')(`
|
|
32
32
|
_ __ ___ __ _ __ _ ___ _ __ ___ _ __ ___ ___ | |_ ___ _ __ _ _ _ __
|
|
33
33
|
| '_ \` _ \\ / _\` |/ _\` |/ _ \\____| '__/ _ \\ '_ \` _ \\ / _ \\| __/ _ \\____| '__| | | | '_ \\
|
|
34
34
|
| | | | | | (_| | (_| | __/____| | | __/ | | | | | (_) | || __/____| | | |_| | | | |
|
|
@@ -38,27 +38,49 @@ program
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
registerConnectionCommands,
|
|
43
|
+
registerCoreCommands,
|
|
44
|
+
registerCloudCommands
|
|
45
|
+
} from '../lib/command-registry.js';
|
|
42
46
|
import { getActiveProfile } from '../lib/config.js';
|
|
47
|
+
import { startMcpServer } from '../lib/mcp.js';
|
|
48
|
+
|
|
49
|
+
registerConnectionCommands(program);
|
|
50
|
+
|
|
51
|
+
program.command('mcp')
|
|
52
|
+
.description('Run as MCP server')
|
|
53
|
+
.option('--transport <type>', 'Transport type (stdio, http)', 'stdio')
|
|
54
|
+
.option('--host <host>', 'HTTP Host', '127.0.0.1')
|
|
55
|
+
.option('--port <port>', 'HTTP Port', '18098')
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
await startMcpServer(options);
|
|
58
|
+
});
|
|
43
59
|
|
|
44
60
|
const profile = await getActiveProfile();
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
if (profile) {
|
|
63
|
+
registerCoreCommands(program);
|
|
64
|
+
|
|
65
|
+
if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
|
|
66
|
+
registerCloudCommands(program);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
47
69
|
|
|
48
70
|
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
71
|
+
// Check if we have an active profile and if format is not json/xml
|
|
72
|
+
// Note: 'options' are available on the command that has them defined.
|
|
73
|
+
// actionCommand is the command actually being executed.
|
|
74
|
+
if (profile) {
|
|
75
|
+
const config = await loadConfig();
|
|
76
|
+
if (config.showActiveConnectionHeader !== false) {
|
|
77
|
+
const opts = actionCommand.opts();
|
|
78
|
+
if (opts.format !== 'json' && opts.format !== 'xml') {
|
|
79
|
+
console.log(chalk.cyan(`Active Connection: ${chalk.bold(profile.name)} (${profile.type})`));
|
|
80
|
+
console.log(chalk.gray('━'.repeat(60)) + '\n');
|
|
81
|
+
}
|
|
61
82
|
}
|
|
83
|
+
}
|
|
62
84
|
});
|
|
63
85
|
|
|
64
86
|
import { expandCommandAbbreviations } from '../lib/command-helper.js';
|
|
@@ -72,28 +94,28 @@ const config = await loadConfig();
|
|
|
72
94
|
const hasProfiles = Object.keys(config.profiles || {}).length > 0;
|
|
73
95
|
|
|
74
96
|
if (!hasProfiles && args.length === 0) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
console.log(chalk.bold.blue('Welcome to mage-remote-run! 🚀'));
|
|
98
|
+
console.log(chalk.gray('The remote swiss army knife for Magento Open Source, Mage-OS, Adobe Commerce'));
|
|
99
|
+
console.log(chalk.gray('It looks like you haven\'t configured any connections yet.'));
|
|
100
|
+
console.log(chalk.gray('Let\'s set up your first connection now.\n'));
|
|
101
|
+
|
|
102
|
+
// Trigger the interactive add command directly
|
|
103
|
+
// We can simulate running the 'connection add' command
|
|
104
|
+
// But since we are at top level, we might need to invoke it manually or parse specific args.
|
|
105
|
+
// Easiest is to manually invoke program.parse with ['node', 'script', 'connection', 'add']
|
|
106
|
+
// BUT program.parse executes asynchronously usually? commander is synchronous by default but actions are async.
|
|
107
|
+
// Let's modify process.argv before parsing.
|
|
108
|
+
args = ['connection', 'add'];
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
try {
|
|
90
|
-
|
|
112
|
+
args = expandCommandAbbreviations(program, args);
|
|
91
113
|
} catch (e) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
if (e.isAmbiguous) {
|
|
115
|
+
console.error(e.message);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
throw e;
|
|
97
119
|
}
|
|
98
120
|
process.argv = [...process.argv.slice(0, 2), ...args];
|
|
99
121
|
|
package/lib/command-registry.js
CHANGED
|
@@ -12,24 +12,57 @@ import { registerAdobeIoEventsCommands } from './commands/adobe-io-events.js';
|
|
|
12
12
|
import { registerWebhooksCommands } from './commands/webhooks.js';
|
|
13
13
|
import { registerConsoleCommand } from './commands/console.js';
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
}
|
|
15
|
+
export { registerConnectionCommands, registerConsoleCommand };
|
|
16
|
+
|
|
17
|
+
export function registerCoreCommands(program) {
|
|
18
|
+
registerWebsitesCommands(program);
|
|
19
|
+
registerStoresCommands(program);
|
|
20
|
+
registerCustomersCommands(program);
|
|
21
|
+
registerOrdersCommands(program);
|
|
22
|
+
registerEavCommands(program);
|
|
23
|
+
registerProductsCommands(program);
|
|
24
|
+
registerTaxCommands(program);
|
|
25
|
+
registerInventoryCommands(program);
|
|
26
|
+
registerConsoleCommand(program);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerCloudCommands(program) {
|
|
30
|
+
registerAdobeIoEventsCommands(program);
|
|
31
|
+
registerCompanyCommands(program);
|
|
32
|
+
registerWebhooksCommands(program);
|
|
35
33
|
}
|
|
34
|
+
|
|
35
|
+
export function registerAllCommands(program) {
|
|
36
|
+
registerConnectionCommands(program);
|
|
37
|
+
registerCoreCommands(program);
|
|
38
|
+
registerCloudCommands(program);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Alias for backwards compatibility with console.js (and potential other usages)
|
|
42
|
+
export const registerCommands = (program, profile) => {
|
|
43
|
+
// Note: console.js passes profile, but registerAllCommands doesn't strictly use it
|
|
44
|
+
// (it registers everything, profile checks happen inside or via selective registration).
|
|
45
|
+
// The original behavior in console.js:
|
|
46
|
+
// const profile = await getActiveProfile();
|
|
47
|
+
// registerCommands(localProgram, profile);
|
|
48
|
+
|
|
49
|
+
// In bin/mage-remote-run.js old logic:
|
|
50
|
+
// registerConnectionCommands(program);
|
|
51
|
+
// if (profile) { registerWebsitesCommands... }
|
|
52
|
+
|
|
53
|
+
// We should replicate that 'selective' registration if we want to match exact behavior?
|
|
54
|
+
// BUT console.js wants to register ALL available commands for the profile.
|
|
55
|
+
|
|
56
|
+
registerConnectionCommands(program);
|
|
57
|
+
|
|
58
|
+
// Simple logic: If profile exists, register all.
|
|
59
|
+
// console.js usage implies profile is present if it's running (or it tries to get it).
|
|
60
|
+
|
|
61
|
+
if (profile) {
|
|
62
|
+
registerCoreCommands(program);
|
|
63
|
+
// Cloud check
|
|
64
|
+
if (profile.type === 'ac-cloud-paas' || profile.type === 'ac-saas') {
|
|
65
|
+
registerCloudCommands(program);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -349,6 +349,10 @@ Examples:
|
|
|
349
349
|
default: config.activeProfile
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
+
if (process.env.DEBUG) {
|
|
353
|
+
console.log(chalk.gray(`DEBUG: Selected profile: ${selected}`));
|
|
354
|
+
}
|
|
355
|
+
|
|
352
356
|
config.activeProfile = selected;
|
|
353
357
|
await saveConfig(config);
|
|
354
358
|
console.log(chalk.green(`Active profile set to "${selected}".`));
|
|
@@ -393,6 +397,8 @@ Examples:
|
|
|
393
397
|
config.showActiveConnectionHeader = state === 'on';
|
|
394
398
|
await saveConfig(config);
|
|
395
399
|
console.log(chalk.green(`Active connection header is now ${state === 'on' ? 'enabled' : 'disabled'}.`));
|
|
396
|
-
} catch (e) {
|
|
400
|
+
} catch (e) {
|
|
401
|
+
handleError(e);
|
|
402
|
+
}
|
|
397
403
|
});
|
|
398
404
|
}
|
package/lib/commands/console.js
CHANGED
|
@@ -25,48 +25,55 @@ export function registerConsoleCommand(program) {
|
|
|
25
25
|
console.log(chalk.gray('Type "list" to see available commands.'));
|
|
26
26
|
console.log(chalk.gray('Type .exit to quit.\n'));
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
throw err;
|
|
28
|
+
// State for the REPL
|
|
29
|
+
let localProgram;
|
|
30
|
+
let currentProfile;
|
|
31
|
+
|
|
32
|
+
// Function to load/reload commands based on current profile
|
|
33
|
+
const loadLocalCommands = async () => {
|
|
34
|
+
// Create a fresh program instance for the REPL
|
|
35
|
+
localProgram = new Command();
|
|
36
|
+
|
|
37
|
+
// Configure custom output for REPL to avoid duplicate error printing
|
|
38
|
+
localProgram.configureOutput({
|
|
39
|
+
writeOut: (str) => process.stdout.write(str),
|
|
40
|
+
writeErr: (str) => process.stderr.write(str),
|
|
42
41
|
});
|
|
43
|
-
// Check for subcommands
|
|
44
|
-
if (cmd.commands) {
|
|
45
|
-
cmd.commands.forEach(applyExitOverride);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
42
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
// Apply exitOverride recursively
|
|
44
|
+
const applyExitOverride = (cmd) => {
|
|
45
|
+
cmd.exitOverride((err) => {
|
|
46
|
+
throw err;
|
|
47
|
+
});
|
|
48
|
+
if (cmd.commands) {
|
|
49
|
+
cmd.commands.forEach(applyExitOverride);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// Get current profile and register commands
|
|
54
|
+
const profile = await getActiveProfile();
|
|
55
|
+
registerCommands(localProgram, profile);
|
|
56
|
+
applyExitOverride(localProgram);
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Update current profile state (store simple unique string for comparison)
|
|
59
|
+
currentProfile = profile ? `${profile.name}:${profile.type}` : 'null';
|
|
60
|
+
|
|
61
|
+
return { localProgram, profile };
|
|
62
|
+
};
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
// Initial load
|
|
65
|
+
await loadLocalCommands();
|
|
62
66
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
67
|
+
// Use 'stream' module to create dummy REPL for capturing defaults
|
|
68
|
+
const { PassThrough } = await import('stream');
|
|
69
|
+
const dummy = repl.start({ input: new PassThrough(), output: new PassThrough(), terminal: false });
|
|
70
|
+
const defaultCompleter = dummy.completer;
|
|
71
|
+
const defaultEval = dummy.eval;
|
|
72
|
+
dummy.close();
|
|
65
73
|
|
|
66
|
-
// Custom completer
|
|
74
|
+
// Custom completer definition
|
|
67
75
|
const myCompleter = (line, callback) => {
|
|
68
76
|
const parts = line.split(/\s+/);
|
|
69
|
-
// Remove empty start if line starts with space (not typical for completion line but safe)
|
|
70
77
|
if (parts[0] === '') parts.shift();
|
|
71
78
|
|
|
72
79
|
let current = '';
|
|
@@ -80,19 +87,16 @@ export function registerConsoleCommand(program) {
|
|
|
80
87
|
contextParts = parts;
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
// Helper to get candidates from a command object
|
|
84
90
|
const getCandidates = (cmdObj) => {
|
|
85
91
|
return cmdObj.commands.map(c => c.name());
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
let hits = [];
|
|
89
95
|
|
|
90
|
-
// Top-level completion (start of line)
|
|
91
96
|
if (contextParts.length === 0) {
|
|
92
97
|
const candidates = getCandidates(localProgram);
|
|
93
98
|
hits = candidates.filter(c => c.startsWith(current));
|
|
94
99
|
} else {
|
|
95
|
-
// Subcommand completion
|
|
96
100
|
let cmd = localProgram;
|
|
97
101
|
let validContext = true;
|
|
98
102
|
|
|
@@ -112,20 +116,18 @@ export function registerConsoleCommand(program) {
|
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
// If we have distinct command hits, return them
|
|
116
119
|
if (hits.length > 0) {
|
|
117
120
|
return callback(null, [hits, current]);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
// Fallback to default JS completion
|
|
121
123
|
return defaultCompleter(line, callback);
|
|
122
124
|
};
|
|
123
125
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
+
// Custom evaluator definition
|
|
127
|
+
const myEval = async function (cmd, context, filename, callback) {
|
|
128
|
+
// 'this' is the REPLServer instance
|
|
129
|
+
const rInstance = this;
|
|
126
130
|
|
|
127
|
-
// Custom evaluator to support both CLI commands and JS
|
|
128
|
-
const myEval = async (cmd, context, filename, callback) => {
|
|
129
131
|
cmd = cmd.trim();
|
|
130
132
|
|
|
131
133
|
if (!cmd) {
|
|
@@ -135,7 +137,6 @@ export function registerConsoleCommand(program) {
|
|
|
135
137
|
|
|
136
138
|
if (cmd === 'list') {
|
|
137
139
|
console.log(chalk.bold('\nAvailable Commands:'));
|
|
138
|
-
// Use localProgram commands
|
|
139
140
|
localProgram.commands.filter(c => !c._hidden).sort((a, b) => a.name().localeCompare(b.name())).forEach(c => {
|
|
140
141
|
console.log(` ${chalk.cyan(c.name().padEnd(25))} ${c.description()}`);
|
|
141
142
|
});
|
|
@@ -151,8 +152,6 @@ export function registerConsoleCommand(program) {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
try {
|
|
154
|
-
// Parse arguments nicely
|
|
155
|
-
// This regex handles quoted arguments
|
|
156
155
|
const args = (cmd.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g) || []).map(arg => {
|
|
157
156
|
if (arg.startsWith('"') && arg.endsWith('"')) return arg.slice(1, -1);
|
|
158
157
|
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
|
|
@@ -171,32 +170,10 @@ export function registerConsoleCommand(program) {
|
|
|
171
170
|
}
|
|
172
171
|
} catch (e) {
|
|
173
172
|
if (e.isAmbiguous) {
|
|
174
|
-
// If ambiguous, print error and return (don't execute JS, as user likelihood meant a command)
|
|
175
|
-
// But wait, what if "c" is a var?
|
|
176
|
-
// If I type "c", it is ambiguous.
|
|
177
|
-
// If I fallback to JS, "c" might work.
|
|
178
|
-
// If I print error, "c" access is blocked.
|
|
179
|
-
|
|
180
|
-
// Compromise: If it LOOKS like a command usage (e.g. multiple args), treat as command error?
|
|
181
|
-
// Or just always fallback to JS if ambiguous?
|
|
182
|
-
// If fallback to JS, user sees "c is not defined".
|
|
183
|
-
// User asks: "Why didn't it run command?". Answer: "Ambiguous".
|
|
184
|
-
// Explicit ambiguity error is arguably better than generic ReferenceError.
|
|
185
|
-
|
|
186
|
-
// However, "c = 1" or "const c = 1"
|
|
187
|
-
// "const" -> ambiguous? "connection", "console"... no "const" is not in list of commands.
|
|
188
|
-
// "const" might NOT match any command prefix if lucky.
|
|
189
|
-
// But if it does...
|
|
190
|
-
|
|
191
|
-
// Let's stick to safe approach: fallback to JS if ambiguous, unless we are SURE it's not JS?
|
|
192
|
-
// Or: Just print the ambiguous error if the first arg matched *something*.
|
|
193
|
-
|
|
194
|
-
// Let's try to assume command first. If ambiguous, show error.
|
|
195
173
|
console.error(chalk.red(e.message));
|
|
196
174
|
callback(null);
|
|
197
175
|
return;
|
|
198
176
|
}
|
|
199
|
-
// Other errors?
|
|
200
177
|
throw e;
|
|
201
178
|
}
|
|
202
179
|
|
|
@@ -205,32 +182,84 @@ export function registerConsoleCommand(program) {
|
|
|
205
182
|
|
|
206
183
|
if (knownCommands.includes(firstWord)) {
|
|
207
184
|
// Valid command found
|
|
185
|
+
let keypressListeners = [];
|
|
208
186
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
187
|
+
rInstance.pause();
|
|
188
|
+
|
|
189
|
+
// Capture and remove keypress listeners to prevent REPL from intercepting input
|
|
190
|
+
// intended for interactive commands (like inquirer)
|
|
191
|
+
keypressListeners = process.stdin.listeners('keypress');
|
|
192
|
+
process.stdin.removeAllListeners('keypress');
|
|
211
193
|
|
|
212
|
-
// Restore REPL state
|
|
213
194
|
if (process.stdin.isTTY) {
|
|
214
|
-
process.stdin.setRawMode(
|
|
195
|
+
process.stdin.setRawMode(false);
|
|
215
196
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
197
|
+
|
|
198
|
+
await localProgram.parseAsync(['node', 'mage-remote-run', ...expandedArgs]);
|
|
199
|
+
|
|
200
|
+
// Check for profile change after command execution
|
|
201
|
+
const newProfileObj = await getActiveProfile();
|
|
202
|
+
const newProfileKey = newProfileObj ? `${newProfileObj.name}:${newProfileObj.type}` : 'null';
|
|
203
|
+
|
|
204
|
+
if (process.env.DEBUG) {
|
|
205
|
+
console.log(chalk.gray(`DEBUG: Check Profile Switch. Current: ${currentProfile}, New: ${newProfileKey}`));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (newProfileKey !== currentProfile) {
|
|
209
|
+
await loadLocalCommands();
|
|
210
|
+
// Update context variables
|
|
211
|
+
rInstance.context.config = await loadConfig();
|
|
212
|
+
|
|
213
|
+
if (process.env.DEBUG) {
|
|
214
|
+
console.log(chalk.green(`\nConnection switched to ${newProfileObj ? newProfileObj.name : 'none'}. Commands reloaded.`));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Restore REPL state
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
// Restore keypress listeners
|
|
221
|
+
keypressListeners.forEach(fn => process.stdin.on('keypress', fn));
|
|
222
|
+
|
|
223
|
+
if (process.stdin.isTTY) {
|
|
224
|
+
process.stdin.setRawMode(true);
|
|
225
|
+
}
|
|
226
|
+
process.stdin.resume();
|
|
227
|
+
|
|
228
|
+
// Flush stdin to remove any buffered leftovers
|
|
229
|
+
let chunk;
|
|
230
|
+
while ((chunk = process.stdin.read()) !== null) { }
|
|
231
|
+
|
|
232
|
+
rInstance.resume();
|
|
233
|
+
rInstance.displayPrompt(true);
|
|
234
|
+
}, 100);
|
|
235
|
+
|
|
219
236
|
} catch (e) {
|
|
237
|
+
// Restore listeners in error case too!
|
|
238
|
+
keypressListeners.forEach(fn => process.stdin.on('keypress', fn));
|
|
239
|
+
|
|
220
240
|
if (process.stdin.isTTY) {
|
|
221
241
|
process.stdin.setRawMode(true);
|
|
222
242
|
}
|
|
223
243
|
process.stdin.resume();
|
|
224
|
-
|
|
244
|
+
rInstance.resume();
|
|
245
|
+
|
|
246
|
+
// Flush stdin
|
|
247
|
+
let chunk;
|
|
248
|
+
while ((chunk = process.stdin.read()) !== null) { }
|
|
249
|
+
|
|
225
250
|
if (e.code === 'commander.helpDisplayed') {
|
|
226
251
|
// Help was displayed, clean exit for us
|
|
252
|
+
setImmediate(() => rInstance.displayPrompt());
|
|
227
253
|
} else if (e.code === 'commander.unknownOption' || e.code === 'commander.unknownCommand') {
|
|
228
254
|
console.error(chalk.red(e.message));
|
|
255
|
+
setImmediate(() => rInstance.displayPrompt());
|
|
229
256
|
} else {
|
|
230
257
|
if (e.code) {
|
|
231
258
|
// Likely a commander exit error we rethrew
|
|
259
|
+
setImmediate(() => rInstance.displayPrompt());
|
|
232
260
|
} else {
|
|
233
261
|
console.error(chalk.red('Command execution error:'), e);
|
|
262
|
+
setImmediate(() => rInstance.displayPrompt());
|
|
234
263
|
}
|
|
235
264
|
}
|
|
236
265
|
}
|
|
@@ -239,27 +268,29 @@ export function registerConsoleCommand(program) {
|
|
|
239
268
|
}
|
|
240
269
|
}
|
|
241
270
|
} catch (e) {
|
|
242
|
-
// unexpected error
|
|
271
|
+
// unexpected error
|
|
243
272
|
}
|
|
244
273
|
|
|
245
274
|
// Fallback to default eval which supports top-level await
|
|
246
|
-
defaultEval.call(
|
|
275
|
+
return defaultEval.call(rInstance, cmd, context, filename, callback);
|
|
247
276
|
};
|
|
248
277
|
|
|
249
|
-
|
|
250
|
-
|
|
278
|
+
const r = repl.start({
|
|
279
|
+
prompt: chalk.green('mage> '),
|
|
280
|
+
eval: myEval,
|
|
281
|
+
completer: myCompleter
|
|
282
|
+
});
|
|
251
283
|
|
|
252
|
-
// Initialize context
|
|
253
284
|
r.context.client = createClient;
|
|
254
285
|
r.context.config = await loadConfig();
|
|
255
286
|
r.context.chalk = chalk;
|
|
256
|
-
r.context.program = program; // Expose global program just in case? Or local?
|
|
257
|
-
r.context.localProgram = localProgram; // Expose local debugging
|
|
258
287
|
|
|
259
288
|
// Helper to reload config if changed
|
|
260
289
|
r.context.reload = async () => {
|
|
261
290
|
r.context.config = await loadConfig();
|
|
262
|
-
|
|
291
|
+
// We should also reload commands here in case a manual config edit changed the type
|
|
292
|
+
await loadLocalCommands();
|
|
293
|
+
console.log(chalk.gray('Config and commands reloaded.'));
|
|
263
294
|
};
|
|
264
295
|
|
|
265
296
|
r.on('exit', () => {
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import http from "http";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
|
|
10
|
+
// Import command registry
|
|
11
|
+
import { registerAllCommands } from './command-registry.js';
|
|
12
|
+
|
|
13
|
+
// Helper to strip ANSI codes for cleaner output
|
|
14
|
+
function stripAnsi(str) {
|
|
15
|
+
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Starts the MCP server.
|
|
20
|
+
* @param {Object} options Configuration options
|
|
21
|
+
* @param {string} options.transport 'stdio' or 'http'
|
|
22
|
+
* @param {string} [options.host] Host for HTTP server
|
|
23
|
+
* @param {number} [options.port] Port for HTTP server
|
|
24
|
+
*/
|
|
25
|
+
export async function startMcpServer(options) {
|
|
26
|
+
const packageJson = JSON.parse(
|
|
27
|
+
readFileSync(new URL("../package.json", import.meta.url))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// 1. Setup a dynamic program to discovery commands
|
|
31
|
+
const program = setupProgram();
|
|
32
|
+
|
|
33
|
+
const server = new McpServer({
|
|
34
|
+
name: "mage-remote-run",
|
|
35
|
+
version: packageJson.version
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const toolsCount = registerTools(server, program);
|
|
39
|
+
|
|
40
|
+
if (options.transport === 'http') {
|
|
41
|
+
const host = options.host || '127.0.0.1';
|
|
42
|
+
const port = options.port || 18098;
|
|
43
|
+
|
|
44
|
+
const transport = new StreamableHTTPServerTransport();
|
|
45
|
+
|
|
46
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
47
|
+
if (req.url === '/sse' || (req.url === '/messages' && req.method === 'POST')) {
|
|
48
|
+
await transport.handleRequest(req, res);
|
|
49
|
+
} else {
|
|
50
|
+
res.writeHead(404);
|
|
51
|
+
res.end();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
httpServer.listen(port, host, () => {
|
|
56
|
+
console.error(`MCP Server running on http://${host}:${port}`);
|
|
57
|
+
console.error(`Protocol: HTTP (SSE)`);
|
|
58
|
+
console.error(`Registered Tools: ${toolsCount}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await server.connect(transport);
|
|
62
|
+
|
|
63
|
+
} else {
|
|
64
|
+
// STDIO
|
|
65
|
+
console.error(`Protocol: stdio`);
|
|
66
|
+
console.error(`Registered Tools: ${toolsCount}`);
|
|
67
|
+
const transport = new StdioServerTransport();
|
|
68
|
+
await server.connect(transport);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function registerTools(server, program) {
|
|
73
|
+
let count = 0;
|
|
74
|
+
|
|
75
|
+
function processCommand(cmd, parentName = '') {
|
|
76
|
+
const cmdName = parentName ? `${parentName}_${cmd.name()}` : cmd.name();
|
|
77
|
+
|
|
78
|
+
// If it has subcommands, process them
|
|
79
|
+
if (cmd.commands && cmd.commands.length > 0) {
|
|
80
|
+
cmd.commands.forEach(subCmd => processCommand(subCmd, cmdName));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// It's a leaf command, register as tool
|
|
85
|
+
// Tool name: Replace spaces/colons with underscores.
|
|
86
|
+
// Example: website list -> website_list
|
|
87
|
+
const toolName = cmdName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
88
|
+
|
|
89
|
+
const schema = {};
|
|
90
|
+
|
|
91
|
+
// Arguments
|
|
92
|
+
// Commander args: cmd._args
|
|
93
|
+
// Options: cmd.options
|
|
94
|
+
|
|
95
|
+
const zodShape = {};
|
|
96
|
+
|
|
97
|
+
cmd._args.forEach(arg => {
|
|
98
|
+
// arg.name(), arg.required
|
|
99
|
+
if (arg.required) {
|
|
100
|
+
zodShape[arg.name()] = z.string().describe(arg.description || '');
|
|
101
|
+
} else {
|
|
102
|
+
zodShape[arg.name()] = z.string().optional().describe(arg.description || '');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
cmd.options.forEach(opt => {
|
|
107
|
+
const name = opt.name(); // e.g. "format" for --format
|
|
108
|
+
// Check flags to guess type.
|
|
109
|
+
// -f, --format <type> -> string
|
|
110
|
+
// -v, --verbose -> boolean
|
|
111
|
+
|
|
112
|
+
if (opt.flags.includes('<')) {
|
|
113
|
+
// Takes an argument, assume string
|
|
114
|
+
zodShape[name] = z.string().optional().describe(opt.description);
|
|
115
|
+
} else {
|
|
116
|
+
// Boolean flag
|
|
117
|
+
zodShape[name] = z.boolean().optional().describe(opt.description);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const description = cmd.description() || `Execute mage-remote-run ${cmdName.replace(/_/g, ' ')}`;
|
|
122
|
+
|
|
123
|
+
server.tool(
|
|
124
|
+
toolName,
|
|
125
|
+
description,
|
|
126
|
+
zodShape,
|
|
127
|
+
async (args) => {
|
|
128
|
+
return await executeCommand(cmd, args, parentName);
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
count++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
program.commands.forEach(cmd => processCommand(cmd));
|
|
136
|
+
return count;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Re-register all commands on a fresh program instance
|
|
140
|
+
// We export this logic so we can reuse it
|
|
141
|
+
function setupProgram() {
|
|
142
|
+
const program = new Command();
|
|
143
|
+
|
|
144
|
+
// Silence output for the main program instance to avoid double printing during parsing
|
|
145
|
+
program.configureOutput({
|
|
146
|
+
writeOut: (str) => { },
|
|
147
|
+
writeErr: (str) => { }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
registerAllCommands(program);
|
|
151
|
+
|
|
152
|
+
return program;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function executeCommand(cmdDefinition, args, parentName) {
|
|
156
|
+
// cmdDefinition is the original command object from discovery, used for context if needed,
|
|
157
|
+
// but here we mainly need the path to find it in the new program.
|
|
158
|
+
// Actually, we can just reconstruct the path from parentName + cmd.name()
|
|
159
|
+
|
|
160
|
+
// Intercept Console
|
|
161
|
+
let output = '';
|
|
162
|
+
const originalLog = console.log;
|
|
163
|
+
const originalError = console.error;
|
|
164
|
+
|
|
165
|
+
// Simple custom logger
|
|
166
|
+
const logInterceptor = (...msgArgs) => {
|
|
167
|
+
const line = msgArgs.map(String).join(' ');
|
|
168
|
+
output += stripAnsi(line) + '\n';
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
console.log = logInterceptor;
|
|
172
|
+
console.error = logInterceptor;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const program = setupProgram();
|
|
176
|
+
|
|
177
|
+
// Construct argv
|
|
178
|
+
// We need to build [node, script, command, subcommand, ..., args, options]
|
|
179
|
+
const argv = ['node', 'mage-remote-run'];
|
|
180
|
+
|
|
181
|
+
// Reconstruct command path
|
|
182
|
+
if (parentName) {
|
|
183
|
+
// parentName might be "website" or "website_domain" (if nested deeper? current logic supports 1 level nesting)
|
|
184
|
+
// Current processCommand logic: `cmdName = parentName ? ${parentName}_${cmd.name()} : cmd.name()`
|
|
185
|
+
// But parentName passed to executeCommand is the prefix.
|
|
186
|
+
// Wait, registerTools calls: `executeCommand(cmd, args, parentName)`
|
|
187
|
+
// If parentName is "website", and cmd is "list", we need "website list"
|
|
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.
|
|
192
|
+
// processCommand(website) -> processCommand(list, "website")
|
|
193
|
+
// parentName in executeCommand is "website".
|
|
194
|
+
// cmd.name() is "list".
|
|
195
|
+
|
|
196
|
+
// However, parentName might contain underscores if deeper nesting?
|
|
197
|
+
// "store_config_list" -> parent "store_config", cmd "list".
|
|
198
|
+
// Commander commands are usually space separated in argv.
|
|
199
|
+
|
|
200
|
+
// We need to parse parentName back to argv tokens?
|
|
201
|
+
// Or better: store the "command path" as an array in the tool context.
|
|
202
|
+
|
|
203
|
+
// Let's rely on standard splitting by underscore, assuming command names don't have underscores.
|
|
204
|
+
// Or we can assume parentName matches the command structure.
|
|
205
|
+
|
|
206
|
+
// Safest: splitting parentName by UNDERSCORE might be risky if command names have underscores.
|
|
207
|
+
// But standard commands here: website, store, etc. don't.
|
|
208
|
+
|
|
209
|
+
// Actually, we can just push parentName, then cmd.name()
|
|
210
|
+
// But parentName comes from `cmdName` variable passed as `parentName` to recursive call.
|
|
211
|
+
// `processCommand(subCmd, cmdName)`
|
|
212
|
+
// `cmdName` = `parentName_cmd.name()`.
|
|
213
|
+
// So for `website list`:
|
|
214
|
+
// `processCommand(website, '')` -> `cmdName="website"`.
|
|
215
|
+
// -> `processCommand(list, "website")`.
|
|
216
|
+
// -> register tool "website_list". `parentName` passed to execute is "website".
|
|
217
|
+
|
|
218
|
+
// So `parentName` is the accumulated prefix with underscores.
|
|
219
|
+
|
|
220
|
+
const parts = parentName.split('_');
|
|
221
|
+
argv.push(...parts);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
argv.push(cmdDefinition.name());
|
|
225
|
+
|
|
226
|
+
// Add arguments and options from the tool args
|
|
227
|
+
|
|
228
|
+
// 1. Positional arguments
|
|
229
|
+
cmdDefinition._args.forEach(arg => {
|
|
230
|
+
if (args[arg.name()]) {
|
|
231
|
+
argv.push(args[arg.name()]);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 2. Options
|
|
236
|
+
cmdDefinition.options.forEach(opt => {
|
|
237
|
+
const name = opt.name(); // e.g. "format"
|
|
238
|
+
if (args[name] !== undefined) {
|
|
239
|
+
const val = args[name];
|
|
240
|
+
if (opt.flags.includes('<')) {
|
|
241
|
+
// String option
|
|
242
|
+
argv.push(`--${name}`);
|
|
243
|
+
argv.push(val);
|
|
244
|
+
} else {
|
|
245
|
+
// Boolean flag
|
|
246
|
+
if (val === true) {
|
|
247
|
+
argv.push(`--${name}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Execute
|
|
254
|
+
await program.parseAsync(argv);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: output }]
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
} catch (e) {
|
|
261
|
+
// Commander throws nicely formatted errors usually, but we suppressed output.
|
|
262
|
+
// If it throws, it might be a cleaner exit error.
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: output + `\nError: ${e.message}` }],
|
|
265
|
+
isError: true
|
|
266
|
+
};
|
|
267
|
+
} finally {
|
|
268
|
+
console.log = originalLog;
|
|
269
|
+
console.error = originalError;
|
|
270
|
+
}
|
|
271
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mage-remote-run",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@inquirer/prompts": "^8.1.0",
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
34
35
|
"axios": "^1.13.2",
|
|
35
36
|
"chalk": "^5.6.2",
|
|
36
37
|
"cli-table3": "^0.6.5",
|
|
@@ -40,7 +41,8 @@
|
|
|
40
41
|
"inquirer": "^13.1.0",
|
|
41
42
|
"mkdirp": "^3.0.1",
|
|
42
43
|
"oauth-1.0a": "^2.2.6",
|
|
43
|
-
"openapi-client-axios": "^7.8.0"
|
|
44
|
+
"openapi-client-axios": "^7.8.0",
|
|
45
|
+
"zod": "^4.2.1"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"jest": "^30.2.0"
|