mage-remote-run 0.12.0 → 0.14.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 +68 -74
- package/lib/command-registry.js +68 -0
- package/lib/commands/adobe-io-events.js +4 -0
- package/lib/commands/company.js +8 -0
- package/lib/commands/connections.js +47 -1
- package/lib/commands/console.js +300 -0
- package/lib/commands/customers.js +28 -0
- package/lib/commands/eav.js +8 -0
- package/lib/commands/inventory.js +20 -0
- package/lib/commands/orders.js +23 -3
- package/lib/commands/products.js +24 -0
- package/lib/commands/stores.js +36 -0
- package/lib/commands/tax.js +8 -0
- package/lib/commands/webhooks.js +4 -0
- package/lib/commands/websites.js +16 -0
- package/lib/mcp.js +263 -0
- package/package.json +4 -2
package/lib/commands/stores.js
CHANGED
|
@@ -9,6 +9,10 @@ export function registerStoresCommands(program) {
|
|
|
9
9
|
// Store Groups
|
|
10
10
|
const groups = stores.command('group').description('Manage store groups');
|
|
11
11
|
|
|
12
|
+
|
|
13
|
+
//-------------------------------------------------------
|
|
14
|
+
// "store group list" Command
|
|
15
|
+
//-------------------------------------------------------
|
|
12
16
|
groups.command('list')
|
|
13
17
|
.description('List store groups')
|
|
14
18
|
.option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
|
|
@@ -40,6 +44,10 @@ Examples:
|
|
|
40
44
|
} catch (e) { handleError(e); }
|
|
41
45
|
});
|
|
42
46
|
|
|
47
|
+
|
|
48
|
+
//-------------------------------------------------------
|
|
49
|
+
// "store group search" Command
|
|
50
|
+
//-------------------------------------------------------
|
|
43
51
|
groups.command('search <query>')
|
|
44
52
|
.description('Search store groups')
|
|
45
53
|
.addHelpText('after', `
|
|
@@ -56,6 +64,10 @@ Examples:
|
|
|
56
64
|
} catch (e) { handleError(e); }
|
|
57
65
|
});
|
|
58
66
|
|
|
67
|
+
|
|
68
|
+
//-------------------------------------------------------
|
|
69
|
+
// "store group delete" Command
|
|
70
|
+
//-------------------------------------------------------
|
|
59
71
|
groups.command('delete <id>')
|
|
60
72
|
.description('Delete store group')
|
|
61
73
|
.addHelpText('after', `
|
|
@@ -72,6 +84,10 @@ Examples:
|
|
|
72
84
|
} catch (e) { handleError(e); }
|
|
73
85
|
});
|
|
74
86
|
|
|
87
|
+
|
|
88
|
+
//-------------------------------------------------------
|
|
89
|
+
// "store group edit" Command
|
|
90
|
+
//-------------------------------------------------------
|
|
75
91
|
groups.command('edit <id>')
|
|
76
92
|
.description('Edit store group')
|
|
77
93
|
.addHelpText('after', `
|
|
@@ -99,6 +115,10 @@ Examples:
|
|
|
99
115
|
// Store Views
|
|
100
116
|
const views = stores.command('view').description('Manage store views');
|
|
101
117
|
|
|
118
|
+
|
|
119
|
+
//-------------------------------------------------------
|
|
120
|
+
// "store view list" Command
|
|
121
|
+
//-------------------------------------------------------
|
|
102
122
|
views.command('list')
|
|
103
123
|
.description('List store views')
|
|
104
124
|
.option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
|
|
@@ -130,6 +150,10 @@ Examples:
|
|
|
130
150
|
} catch (e) { handleError(e); }
|
|
131
151
|
});
|
|
132
152
|
|
|
153
|
+
|
|
154
|
+
//-------------------------------------------------------
|
|
155
|
+
// "store view search" Command
|
|
156
|
+
//-------------------------------------------------------
|
|
133
157
|
views.command('search <query>')
|
|
134
158
|
.description('Search store views')
|
|
135
159
|
.addHelpText('after', `
|
|
@@ -146,6 +170,10 @@ Examples:
|
|
|
146
170
|
} catch (e) { handleError(e); }
|
|
147
171
|
});
|
|
148
172
|
|
|
173
|
+
|
|
174
|
+
//-------------------------------------------------------
|
|
175
|
+
// "store view delete" Command
|
|
176
|
+
//-------------------------------------------------------
|
|
149
177
|
views.command('delete <id>')
|
|
150
178
|
.description('Delete store view')
|
|
151
179
|
.addHelpText('after', `
|
|
@@ -162,6 +190,10 @@ Examples:
|
|
|
162
190
|
} catch (e) { handleError(e); }
|
|
163
191
|
});
|
|
164
192
|
|
|
193
|
+
|
|
194
|
+
//-------------------------------------------------------
|
|
195
|
+
// "store view edit" Command
|
|
196
|
+
//-------------------------------------------------------
|
|
165
197
|
views.command('edit <id>')
|
|
166
198
|
.description('Edit store view')
|
|
167
199
|
.addHelpText('after', `
|
|
@@ -189,6 +221,10 @@ Examples:
|
|
|
189
221
|
// Store Configs
|
|
190
222
|
const configs = stores.command('config').description('Manage store configurations');
|
|
191
223
|
|
|
224
|
+
|
|
225
|
+
//-------------------------------------------------------
|
|
226
|
+
// "store config list" Command
|
|
227
|
+
//-------------------------------------------------------
|
|
192
228
|
configs.command('list')
|
|
193
229
|
.description('List store configurations')
|
|
194
230
|
.addHelpText('after', `
|
package/lib/commands/tax.js
CHANGED
|
@@ -7,6 +7,10 @@ export function registerTaxCommands(program) {
|
|
|
7
7
|
|
|
8
8
|
const taxClass = tax.command('class').description('Manage tax classes');
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
//-------------------------------------------------------
|
|
12
|
+
// "tax class list" Command
|
|
13
|
+
//-------------------------------------------------------
|
|
10
14
|
taxClass.command('list')
|
|
11
15
|
.description('List tax classes')
|
|
12
16
|
.option('-p, --page <number>', 'Page number', '1')
|
|
@@ -49,6 +53,10 @@ Examples:
|
|
|
49
53
|
} catch (e) { handleError(e); }
|
|
50
54
|
});
|
|
51
55
|
|
|
56
|
+
|
|
57
|
+
//-------------------------------------------------------
|
|
58
|
+
// "tax class show" Command
|
|
59
|
+
//-------------------------------------------------------
|
|
52
60
|
taxClass.command('show <id>')
|
|
53
61
|
.description('Show tax class details')
|
|
54
62
|
.addHelpText('after', `
|
package/lib/commands/webhooks.js
CHANGED
|
@@ -5,6 +5,10 @@ import chalk from 'chalk';
|
|
|
5
5
|
export function registerWebhooksCommands(program) {
|
|
6
6
|
const webhooks = program.command('webhook').description('Manage webhooks');
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
//-------------------------------------------------------
|
|
10
|
+
// "webhook list" Command
|
|
11
|
+
//-------------------------------------------------------
|
|
8
12
|
webhooks.command('list')
|
|
9
13
|
.description('List available webhooks')
|
|
10
14
|
.option('-p, --page <number>', 'Page number', '1')
|
package/lib/commands/websites.js
CHANGED
|
@@ -6,6 +6,10 @@ import inquirer from 'inquirer';
|
|
|
6
6
|
export function registerWebsitesCommands(program) {
|
|
7
7
|
const websites = program.command('website').description('Manage websites');
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
//-------------------------------------------------------
|
|
11
|
+
// "website list" Command
|
|
12
|
+
//-------------------------------------------------------
|
|
9
13
|
websites.command('list')
|
|
10
14
|
.description('List all websites')
|
|
11
15
|
.option('-f, --format <type>', 'Output format (text, json, xml)', 'text')
|
|
@@ -37,6 +41,10 @@ Examples:
|
|
|
37
41
|
} catch (e) { handleError(e); }
|
|
38
42
|
});
|
|
39
43
|
|
|
44
|
+
|
|
45
|
+
//-------------------------------------------------------
|
|
46
|
+
// "website search" Command
|
|
47
|
+
//-------------------------------------------------------
|
|
40
48
|
websites.command('search <query>')
|
|
41
49
|
.description('Search websites by code or name (local filter)')
|
|
42
50
|
.addHelpText('after', `
|
|
@@ -55,6 +63,10 @@ Examples:
|
|
|
55
63
|
} catch (e) { handleError(e); }
|
|
56
64
|
});
|
|
57
65
|
|
|
66
|
+
|
|
67
|
+
//-------------------------------------------------------
|
|
68
|
+
// "website delete" Command
|
|
69
|
+
//-------------------------------------------------------
|
|
58
70
|
websites.command('delete <id>')
|
|
59
71
|
.description('Delete a website by ID')
|
|
60
72
|
.addHelpText('after', `
|
|
@@ -77,6 +89,10 @@ Examples:
|
|
|
77
89
|
} catch (e) { handleError(e); }
|
|
78
90
|
});
|
|
79
91
|
|
|
92
|
+
|
|
93
|
+
//-------------------------------------------------------
|
|
94
|
+
// "website edit" Command
|
|
95
|
+
//-------------------------------------------------------
|
|
80
96
|
websites.command('edit <id>')
|
|
81
97
|
.description('Edit a website')
|
|
82
98
|
.addHelpText('after', `
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
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
|
+
|
|
9
|
+
// Import command registry
|
|
10
|
+
import { registerAllCommands } from './command-registry.js';
|
|
11
|
+
|
|
12
|
+
// Helper to strip ANSI codes for cleaner output
|
|
13
|
+
function stripAnsi(str) {
|
|
14
|
+
return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Starts the MCP server.
|
|
19
|
+
* @param {Object} options Configuration options
|
|
20
|
+
* @param {string} options.transport 'stdio' or 'http'
|
|
21
|
+
* @param {string} [options.host] Host for HTTP server
|
|
22
|
+
* @param {number} [options.port] Port for HTTP server
|
|
23
|
+
*/
|
|
24
|
+
export async function startMcpServer(options) {
|
|
25
|
+
// 1. Setup a dynamic program to discovery commands
|
|
26
|
+
const program = setupProgram();
|
|
27
|
+
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: "mage-remote-run",
|
|
30
|
+
version: "1.0.0"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const toolsCount = registerTools(server, program);
|
|
34
|
+
|
|
35
|
+
if (options.transport === 'http') {
|
|
36
|
+
const host = options.host || '127.0.0.1';
|
|
37
|
+
const port = options.port || 18098;
|
|
38
|
+
|
|
39
|
+
const transport = new StreamableHTTPServerTransport();
|
|
40
|
+
|
|
41
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
42
|
+
if (req.url === '/sse' || (req.url === '/messages' && req.method === 'POST')) {
|
|
43
|
+
await transport.handleRequest(req, res);
|
|
44
|
+
} else {
|
|
45
|
+
res.writeHead(404);
|
|
46
|
+
res.end();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
httpServer.listen(port, host, () => {
|
|
51
|
+
console.error(`MCP Server running on http://${host}:${port}`);
|
|
52
|
+
console.error(`Protocol: HTTP (SSE)`);
|
|
53
|
+
console.error(`Registered Tools: ${toolsCount}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await server.connect(transport);
|
|
57
|
+
|
|
58
|
+
} else {
|
|
59
|
+
// STDIO
|
|
60
|
+
console.error(`Protocol: stdio`);
|
|
61
|
+
console.error(`Registered Tools: ${toolsCount}`);
|
|
62
|
+
const transport = new StdioServerTransport();
|
|
63
|
+
await server.connect(transport);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registerTools(server, program) {
|
|
68
|
+
let count = 0;
|
|
69
|
+
|
|
70
|
+
function processCommand(cmd, parentName = '') {
|
|
71
|
+
const cmdName = parentName ? `${parentName}_${cmd.name()}` : cmd.name();
|
|
72
|
+
|
|
73
|
+
// If it has subcommands, process them
|
|
74
|
+
if (cmd.commands && cmd.commands.length > 0) {
|
|
75
|
+
cmd.commands.forEach(subCmd => processCommand(subCmd, cmdName));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// It's a leaf command, register as tool
|
|
80
|
+
// Tool name: Replace spaces/colons with underscores.
|
|
81
|
+
// Example: website list -> website_list
|
|
82
|
+
const toolName = cmdName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
83
|
+
|
|
84
|
+
const schema = {};
|
|
85
|
+
|
|
86
|
+
// Arguments
|
|
87
|
+
// Commander args: cmd._args
|
|
88
|
+
// Options: cmd.options
|
|
89
|
+
|
|
90
|
+
const zodShape = {};
|
|
91
|
+
|
|
92
|
+
cmd._args.forEach(arg => {
|
|
93
|
+
// arg.name(), arg.required
|
|
94
|
+
if (arg.required) {
|
|
95
|
+
zodShape[arg.name()] = z.string().describe(arg.description || '');
|
|
96
|
+
} else {
|
|
97
|
+
zodShape[arg.name()] = z.string().optional().describe(arg.description || '');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
cmd.options.forEach(opt => {
|
|
102
|
+
const name = opt.name(); // e.g. "format" for --format
|
|
103
|
+
// Check flags to guess type.
|
|
104
|
+
// -f, --format <type> -> string
|
|
105
|
+
// -v, --verbose -> boolean
|
|
106
|
+
|
|
107
|
+
if (opt.flags.includes('<')) {
|
|
108
|
+
// Takes an argument, assume string
|
|
109
|
+
zodShape[name] = z.string().optional().describe(opt.description);
|
|
110
|
+
} else {
|
|
111
|
+
// Boolean flag
|
|
112
|
+
zodShape[name] = z.boolean().optional().describe(opt.description);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
server.tool(
|
|
117
|
+
toolName,
|
|
118
|
+
zodShape,
|
|
119
|
+
async (args) => {
|
|
120
|
+
return await executeCommand(cmd, args, parentName);
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
count++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
program.commands.forEach(cmd => processCommand(cmd));
|
|
128
|
+
return count;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Re-register all commands on a fresh program instance
|
|
132
|
+
// We export this logic so we can reuse it
|
|
133
|
+
function setupProgram() {
|
|
134
|
+
const program = new Command();
|
|
135
|
+
|
|
136
|
+
// Silence output for the main program instance to avoid double printing during parsing
|
|
137
|
+
program.configureOutput({
|
|
138
|
+
writeOut: (str) => { },
|
|
139
|
+
writeErr: (str) => { }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
registerAllCommands(program);
|
|
143
|
+
|
|
144
|
+
return program;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeCommand(cmdDefinition, args, parentName) {
|
|
148
|
+
// cmdDefinition is the original command object from discovery, used for context if needed,
|
|
149
|
+
// but here we mainly need the path to find it in the new program.
|
|
150
|
+
// Actually, we can just reconstruct the path from parentName + cmd.name()
|
|
151
|
+
|
|
152
|
+
// Intercept Console
|
|
153
|
+
let output = '';
|
|
154
|
+
const originalLog = console.log;
|
|
155
|
+
const originalError = console.error;
|
|
156
|
+
|
|
157
|
+
// Simple custom logger
|
|
158
|
+
const logInterceptor = (...msgArgs) => {
|
|
159
|
+
const line = msgArgs.map(String).join(' ');
|
|
160
|
+
output += stripAnsi(line) + '\n';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
console.log = logInterceptor;
|
|
164
|
+
console.error = logInterceptor;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const program = setupProgram();
|
|
168
|
+
|
|
169
|
+
// Construct argv
|
|
170
|
+
// We need to build [node, script, command, subcommand, ..., args, options]
|
|
171
|
+
const argv = ['node', 'mage-remote-run'];
|
|
172
|
+
|
|
173
|
+
// Reconstruct command path
|
|
174
|
+
if (parentName) {
|
|
175
|
+
// parentName might be "website" or "website_domain" (if nested deeper? current logic supports 1 level nesting)
|
|
176
|
+
// Current processCommand logic: `cmdName = parentName ? ${parentName}_${cmd.name()} : cmd.name()`
|
|
177
|
+
// But parentName passed to executeCommand is the prefix.
|
|
178
|
+
// Wait, registerTools calls: `executeCommand(cmd, args, parentName)`
|
|
179
|
+
// If parentName is "website", and cmd is "list", we need "website list"
|
|
180
|
+
|
|
181
|
+
// NOTE: parentName in processCommand is built recursively with underscores?
|
|
182
|
+
// In processCommand(subCmd, cmdName), cmdName is "parent_sub".
|
|
183
|
+
// So if we have website -> list.
|
|
184
|
+
// processCommand(website) -> processCommand(list, "website")
|
|
185
|
+
// parentName in executeCommand is "website".
|
|
186
|
+
// cmd.name() is "list".
|
|
187
|
+
|
|
188
|
+
// However, parentName might contain underscores if deeper nesting?
|
|
189
|
+
// "store_config_list" -> parent "store_config", cmd "list".
|
|
190
|
+
// Commander commands are usually space separated in argv.
|
|
191
|
+
|
|
192
|
+
// We need to parse parentName back to argv tokens?
|
|
193
|
+
// Or better: store the "command path" as an array in the tool context.
|
|
194
|
+
|
|
195
|
+
// Let's rely on standard splitting by underscore, assuming command names don't have underscores.
|
|
196
|
+
// Or we can assume parentName matches the command structure.
|
|
197
|
+
|
|
198
|
+
// Safest: splitting parentName by UNDERSCORE might be risky if command names have underscores.
|
|
199
|
+
// But standard commands here: website, store, etc. don't.
|
|
200
|
+
|
|
201
|
+
// Actually, we can just push parentName, then cmd.name()
|
|
202
|
+
// But parentName comes from `cmdName` variable passed as `parentName` to recursive call.
|
|
203
|
+
// `processCommand(subCmd, cmdName)`
|
|
204
|
+
// `cmdName` = `parentName_cmd.name()`.
|
|
205
|
+
// So for `website list`:
|
|
206
|
+
// `processCommand(website, '')` -> `cmdName="website"`.
|
|
207
|
+
// -> `processCommand(list, "website")`.
|
|
208
|
+
// -> register tool "website_list". `parentName` passed to execute is "website".
|
|
209
|
+
|
|
210
|
+
// So `parentName` is the accumulated prefix with underscores.
|
|
211
|
+
|
|
212
|
+
const parts = parentName.split('_');
|
|
213
|
+
argv.push(...parts);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
argv.push(cmdDefinition.name());
|
|
217
|
+
|
|
218
|
+
// Add arguments and options from the tool args
|
|
219
|
+
|
|
220
|
+
// 1. Positional arguments
|
|
221
|
+
cmdDefinition._args.forEach(arg => {
|
|
222
|
+
if (args[arg.name()]) {
|
|
223
|
+
argv.push(args[arg.name()]);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 2. Options
|
|
228
|
+
cmdDefinition.options.forEach(opt => {
|
|
229
|
+
const name = opt.name(); // e.g. "format"
|
|
230
|
+
if (args[name] !== undefined) {
|
|
231
|
+
const val = args[name];
|
|
232
|
+
if (opt.flags.includes('<')) {
|
|
233
|
+
// String option
|
|
234
|
+
argv.push(`--${name}`);
|
|
235
|
+
argv.push(val);
|
|
236
|
+
} else {
|
|
237
|
+
// Boolean flag
|
|
238
|
+
if (val === true) {
|
|
239
|
+
argv.push(`--${name}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Execute
|
|
246
|
+
await program.parseAsync(argv);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: output }]
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
} catch (e) {
|
|
253
|
+
// Commander throws nicely formatted errors usually, but we suppressed output.
|
|
254
|
+
// If it throws, it might be a cleaner exit error.
|
|
255
|
+
return {
|
|
256
|
+
content: [{ type: "text", text: output + `\nError: ${e.message}` }],
|
|
257
|
+
isError: true
|
|
258
|
+
};
|
|
259
|
+
} finally {
|
|
260
|
+
console.log = originalLog;
|
|
261
|
+
console.error = originalError;
|
|
262
|
+
}
|
|
263
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mage-remote-run",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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"
|