eva4j 1.0.13 → 1.0.14
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/AGENTS.md +51 -9
- package/DOMAIN_YAML_GUIDE.md +150 -0
- package/bin/eva4j.js +31 -1
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +542 -0
- package/docs/commands/GENERATE_ENTITIES.md +196 -0
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/system.yaml +289 -0
- package/package.json +1 -1
- package/src/commands/create.js +6 -3
- package/src/commands/evaluate-system.js +384 -0
- package/src/commands/generate-entities.js +677 -14
- package/src/commands/generate-kafka-event.js +59 -5
- package/src/commands/generate-system.js +243 -0
- package/src/generators/base-generator.js +9 -1
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +314 -0
- package/src/utils/yaml-to-entity.js +31 -2
- package/templates/aggregate/AggregateRepository.java.ejs +5 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
- package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
- package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
- package/templates/base/root/skill-build-system-yaml.ejs +252 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +12 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/evaluate/report.html.ejs +971 -0
- package/templates/kafka-event/Event.java.ejs +7 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const ejs = require('ejs');
|
|
9
|
+
const ora = require('ora');
|
|
10
|
+
|
|
11
|
+
const { validateSystem } = require('../utils/system-validator');
|
|
12
|
+
|
|
13
|
+
// ── Module icon heuristic ────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const ICON_RULES = [
|
|
16
|
+
[/payment|billing|invoice|charge/i, '💳'],
|
|
17
|
+
[/notification|alert|email|sms|message|notify/i, '🔔'],
|
|
18
|
+
[/customer|user|account|profile|member|client/i, '👤'],
|
|
19
|
+
[/movie|film|cinema|content|catalog|media/i, '🎬'],
|
|
20
|
+
[/theater|venue|seat|hall|screen/i, '🏛️'],
|
|
21
|
+
[/reservation|booking|ticket|order/i, '🎟️'],
|
|
22
|
+
[/product|item|inventory|catalog|stock/i, '🛍️'],
|
|
23
|
+
[/shipping|delivery|logistics|warehouse/i, '📦'],
|
|
24
|
+
[/auth|security|identity|session/i, '🔐'],
|
|
25
|
+
[/report|analytics|metric|stat/i, '📊'],
|
|
26
|
+
[/search|index|discover/i, '🔍'],
|
|
27
|
+
[/screening|schedule|program|event/i, '📽️'],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const COLOR_PALETTE = [
|
|
31
|
+
'#4a9eff', // blue
|
|
32
|
+
'#9b6dff', // purple
|
|
33
|
+
'#f5c842', // gold
|
|
34
|
+
'#2dcc8f', // green
|
|
35
|
+
'#ff8c42', // orange
|
|
36
|
+
'#e63950', // red/accent
|
|
37
|
+
'#40c4d0', // teal
|
|
38
|
+
'#ff6bac', // pink
|
|
39
|
+
'#a8e063', // lime
|
|
40
|
+
'#ffa07a', // salmon
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function assignIcon(name) {
|
|
44
|
+
for (const [pattern, icon] of ICON_RULES) {
|
|
45
|
+
if (pattern.test(name)) return icon;
|
|
46
|
+
}
|
|
47
|
+
return '📁';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Flow auto-generation from async events ───────────────────────────────────
|
|
51
|
+
|
|
52
|
+
// Maps trailing verb in an event name to the action verb used in useCases
|
|
53
|
+
const EVENT_VERB_MAP = {
|
|
54
|
+
created: 'Create',
|
|
55
|
+
confirmed: 'Confirm',
|
|
56
|
+
approved: 'Approve',
|
|
57
|
+
rejected: 'Reject',
|
|
58
|
+
cancelled: 'Cancel',
|
|
59
|
+
canceled: 'Cancel',
|
|
60
|
+
locked: 'Lock',
|
|
61
|
+
unlocked: 'Unlock',
|
|
62
|
+
expired: 'Expire',
|
|
63
|
+
scheduled: 'Schedule',
|
|
64
|
+
processed: 'Process',
|
|
65
|
+
published: 'Publish',
|
|
66
|
+
updated: 'Update',
|
|
67
|
+
deleted: 'Delete',
|
|
68
|
+
completed: 'Complete',
|
|
69
|
+
failed: 'Fail',
|
|
70
|
+
started: 'Start',
|
|
71
|
+
initiated: 'Initiate',
|
|
72
|
+
activated: 'Activate',
|
|
73
|
+
deactivated: 'Deactivate',
|
|
74
|
+
registered: 'Register',
|
|
75
|
+
requested: 'Request',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function extractEventVerb(eventName) {
|
|
79
|
+
// e.g. "ReservationCreatedEvent" → "created" → "Create"
|
|
80
|
+
const withoutSuffix = eventName.replace(/Event$/, '');
|
|
81
|
+
const parts = withoutSuffix.split(/(?=[A-Z])/); // split on uppercase
|
|
82
|
+
const lastWord = parts[parts.length - 1].toLowerCase();
|
|
83
|
+
return EVENT_VERB_MAP[lastWord] || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractEventSubject(eventName) {
|
|
87
|
+
// e.g. "ReservationCreatedEvent" → "Reservation"
|
|
88
|
+
const withoutSuffix = eventName.replace(/Event$/, '');
|
|
89
|
+
const verb = extractEventVerb(eventName);
|
|
90
|
+
if (!verb) return withoutSuffix;
|
|
91
|
+
const verbKey = Object.keys(EVENT_VERB_MAP).find(
|
|
92
|
+
(k) => EVENT_VERB_MAP[k] === verb
|
|
93
|
+
);
|
|
94
|
+
if (!verbKey) return withoutSuffix;
|
|
95
|
+
// Remove the trailing verb word from the event name
|
|
96
|
+
const verbCamel = verbKey.charAt(0).toUpperCase() + verbKey.slice(1);
|
|
97
|
+
return withoutSuffix.replace(new RegExp(verbCamel + '$', 'i'), '');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findTriggerEndpoint(verb, producerName, modulesConfig) {
|
|
101
|
+
if (!verb) return null;
|
|
102
|
+
const mod = modulesConfig.find((m) => m.name === producerName);
|
|
103
|
+
if (!mod) return null;
|
|
104
|
+
return (mod.exposes || []).find((ep) => {
|
|
105
|
+
const uc = ep.useCase || '';
|
|
106
|
+
return uc.toLowerCase().startsWith(verb.toLowerCase()) || uc.includes(verb);
|
|
107
|
+
}) || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildEventFlows(systemConfig, modulesMap) {
|
|
111
|
+
const asyncEvents = (systemConfig.integrations || {}).async || [];
|
|
112
|
+
const syncIntegrations = (systemConfig.integrations || {}).sync || [];
|
|
113
|
+
const modulesConfig = systemConfig.modules || [];
|
|
114
|
+
|
|
115
|
+
const flows = [];
|
|
116
|
+
|
|
117
|
+
for (const ev of asyncEvents) {
|
|
118
|
+
const verb = extractEventVerb(ev.event);
|
|
119
|
+
const subject = extractEventSubject(ev.event);
|
|
120
|
+
const triggerEndpoint = findTriggerEndpoint(verb, ev.producer, modulesConfig);
|
|
121
|
+
|
|
122
|
+
const producerMod = modulesMap[ev.producer] || { color: '#888888', label: ev.producer, icon: '📁' };
|
|
123
|
+
const consumers = (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module));
|
|
124
|
+
|
|
125
|
+
// Find sync calls made by this producer module that might be part of this action
|
|
126
|
+
const producerSyncCalls = syncIntegrations.filter((s) => s.caller === ev.producer);
|
|
127
|
+
|
|
128
|
+
const steps = [];
|
|
129
|
+
|
|
130
|
+
// Step 1: HTTP trigger (from client to producer)
|
|
131
|
+
if (triggerEndpoint) {
|
|
132
|
+
const syncCallsForStep = producerSyncCalls.map((s) => ({
|
|
133
|
+
to: s.calls,
|
|
134
|
+
label: (s.using || [])[0] || `GET /${s.calls}`,
|
|
135
|
+
port: s.port,
|
|
136
|
+
}));
|
|
137
|
+
steps.push({
|
|
138
|
+
id: 1,
|
|
139
|
+
type: 'http',
|
|
140
|
+
from: 'client',
|
|
141
|
+
to: ev.producer,
|
|
142
|
+
label: `${triggerEndpoint.method} ${triggerEndpoint.path}`,
|
|
143
|
+
desc: triggerEndpoint.description || `${verb} ${subject}`,
|
|
144
|
+
syncCalls: syncCallsForStep.length > 0 ? syncCallsForStep : undefined,
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
steps.push({
|
|
148
|
+
id: 1,
|
|
149
|
+
type: 'http',
|
|
150
|
+
from: 'client',
|
|
151
|
+
to: ev.producer,
|
|
152
|
+
label: `${verb || 'trigger'} /${subject.toLowerCase()}`,
|
|
153
|
+
desc: `Acción que desencadena el evento`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Step 2: Kafka event
|
|
158
|
+
steps.push({
|
|
159
|
+
id: 2,
|
|
160
|
+
type: 'event',
|
|
161
|
+
from: ev.producer,
|
|
162
|
+
event: ev.event,
|
|
163
|
+
topic: ev.topic,
|
|
164
|
+
to: consumers,
|
|
165
|
+
desc: `${ev.event} publicado en Kafka (topic: ${ev.topic})`,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Step 3+: Consumer actions
|
|
169
|
+
for (let i = 0; i < consumers.length; i++) {
|
|
170
|
+
const consumer = consumers[i];
|
|
171
|
+
const consumerMod = modulesConfig.find((m) => m.name === consumer);
|
|
172
|
+
// Find a likely endpoint that the consumer would trigger on receiving this event
|
|
173
|
+
let actionLabel = `Procesa ${ev.event}`;
|
|
174
|
+
if (consumerMod) {
|
|
175
|
+
const verbLower = (verb || '').toLowerCase();
|
|
176
|
+
const match = (consumerMod.exposes || []).find((ep) => {
|
|
177
|
+
const uc = (ep.useCase || '').toLowerCase();
|
|
178
|
+
const method = (ep.method || '').toUpperCase();
|
|
179
|
+
return (uc.includes(verbLower) || uc.includes(subject.toLowerCase())) &&
|
|
180
|
+
(method === 'PUT' || method === 'PATCH' || method === 'POST');
|
|
181
|
+
});
|
|
182
|
+
if (match) {
|
|
183
|
+
actionLabel = `${match.useCase} (${match.method} ${match.path})`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
steps.push({
|
|
187
|
+
id: i + 3,
|
|
188
|
+
type: 'action',
|
|
189
|
+
from: consumer,
|
|
190
|
+
to: consumer,
|
|
191
|
+
label: actionLabel,
|
|
192
|
+
desc: `${consumer} reacciona al evento ${ev.event}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
flows.push({
|
|
197
|
+
id: ev.event,
|
|
198
|
+
label: ev.event.replace(/Event$/, '').replace(/([A-Z])/g, ' $1').trim(),
|
|
199
|
+
icon: producerMod.icon || '📨',
|
|
200
|
+
description: `${ev.producer} → [${consumers.join(', ')}] vía topic ${ev.topic}`,
|
|
201
|
+
color: producerMod.color,
|
|
202
|
+
steps,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return flows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Data extraction ──────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
function extractReportData(systemConfig, validation) {
|
|
212
|
+
const modulesConfig = systemConfig.modules || [];
|
|
213
|
+
const asyncEvents = (systemConfig.integrations || {}).async || [];
|
|
214
|
+
const syncIntegrations = (systemConfig.integrations || {}).sync || [];
|
|
215
|
+
|
|
216
|
+
// Build modules map with color + icon
|
|
217
|
+
const modulesMap = {};
|
|
218
|
+
for (let i = 0; i < modulesConfig.length; i++) {
|
|
219
|
+
const mod = modulesConfig[i];
|
|
220
|
+
modulesMap[mod.name] = {
|
|
221
|
+
id: mod.name,
|
|
222
|
+
label: toPascalCase(mod.name),
|
|
223
|
+
icon: assignIcon(mod.name),
|
|
224
|
+
color: COLOR_PALETTE[i % COLOR_PALETTE.length],
|
|
225
|
+
desc: mod.description || mod.name,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Normalize events (consumers can be strings or objects with .module)
|
|
230
|
+
const events = asyncEvents.map((ev) => ({
|
|
231
|
+
event: ev.event,
|
|
232
|
+
producer: ev.producer,
|
|
233
|
+
topic: ev.topic,
|
|
234
|
+
consumers: (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module)),
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
// Normalize sync integrations
|
|
238
|
+
const syncList = syncIntegrations.map((s) => ({
|
|
239
|
+
caller: s.caller,
|
|
240
|
+
calls: s.calls,
|
|
241
|
+
port: s.port || `${toPascalCase(s.calls)}Service`,
|
|
242
|
+
endpoints: s.using || [],
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
// Build endpoints per module
|
|
246
|
+
const endpoints = {};
|
|
247
|
+
for (const mod of modulesConfig) {
|
|
248
|
+
endpoints[mod.name] = (mod.exposes || []).map((ep) => `${ep.method} ${ep.path}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Auto-generate flows
|
|
252
|
+
const flows = buildEventFlows(systemConfig, modulesMap);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
systemName: (systemConfig.system || {}).name || 'eva4j system',
|
|
256
|
+
modules: Object.values(modulesMap),
|
|
257
|
+
events,
|
|
258
|
+
syncIntegrations: syncList,
|
|
259
|
+
endpoints,
|
|
260
|
+
flows,
|
|
261
|
+
validation,
|
|
262
|
+
generatedAt: new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Command ──────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
async function evaluateSystemCommand(type, options = {}) {
|
|
269
|
+
if (type !== 'system') {
|
|
270
|
+
console.error(chalk.red(`❌ Unknown evaluation type: '${type}'`));
|
|
271
|
+
console.log(chalk.gray("Usage: eva evaluate system"));
|
|
272
|
+
console.log(chalk.gray("Only 'system' is supported at this time."));
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const port = parseInt(options.port || '3000', 10);
|
|
277
|
+
const outputPath = path.resolve(process.cwd(), options.output || './system-report.html');
|
|
278
|
+
|
|
279
|
+
// ── 1. Read system.yaml ─────────────────────────────────────────────────
|
|
280
|
+
const systemYamlPath = path.join(process.cwd(), 'system.yaml');
|
|
281
|
+
if (!(await fs.pathExists(systemYamlPath))) {
|
|
282
|
+
console.error(chalk.red('❌ system.yaml not found in current directory'));
|
|
283
|
+
console.error(chalk.gray('Run this command from the root of an eva4j project'));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let systemConfig;
|
|
288
|
+
try {
|
|
289
|
+
const content = await fs.readFile(systemYamlPath, 'utf-8');
|
|
290
|
+
systemConfig = yaml.load(content);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(chalk.red('❌ Failed to parse system.yaml:'), err.message);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const spinner = ora('Analyzing system.yaml...').start();
|
|
297
|
+
|
|
298
|
+
// ── 2. Run validation ───────────────────────────────────────────────────
|
|
299
|
+
const validation = validateSystem(systemConfig);
|
|
300
|
+
|
|
301
|
+
// ── 3. Extract report data ──────────────────────────────────────────────
|
|
302
|
+
const reportData = extractReportData(systemConfig, validation);
|
|
303
|
+
|
|
304
|
+
// ── 4. Render HTML ──────────────────────────────────────────────────────
|
|
305
|
+
const templatePath = path.join(__dirname, '../../templates/evaluate/report.html.ejs');
|
|
306
|
+
let htmlContent;
|
|
307
|
+
try {
|
|
308
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
309
|
+
htmlContent = ejs.render(templateContent, { data: reportData });
|
|
310
|
+
} catch (err) {
|
|
311
|
+
spinner.fail('Failed to render HTML template');
|
|
312
|
+
console.error(chalk.red(err.message));
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── 5. Write HTML file ──────────────────────────────────────────────────
|
|
317
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
318
|
+
await fs.writeFile(outputPath, htmlContent, 'utf-8');
|
|
319
|
+
|
|
320
|
+
spinner.succeed(chalk.green('Analysis complete!'));
|
|
321
|
+
|
|
322
|
+
// ── 6. Print validation summary ─────────────────────────────────────────
|
|
323
|
+
console.log();
|
|
324
|
+
console.log(chalk.bold('📊 Validation Summary'));
|
|
325
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
326
|
+
console.log(
|
|
327
|
+
` ${chalk.red('🔴 Errors:')} ${chalk.red.bold(validation.errors.length)}`
|
|
328
|
+
);
|
|
329
|
+
console.log(
|
|
330
|
+
` ${chalk.yellow('🟡 Warnings:')} ${chalk.yellow.bold(validation.warnings.length)}`
|
|
331
|
+
);
|
|
332
|
+
console.log(
|
|
333
|
+
` ${chalk.green('🟢 Passed:')} ${chalk.green.bold(validation.ok.length)}`
|
|
334
|
+
);
|
|
335
|
+
console.log(
|
|
336
|
+
` ${chalk.blue('📈 Score:')} ${chalk.blue.bold(validation.score + '%')}`
|
|
337
|
+
);
|
|
338
|
+
console.log();
|
|
339
|
+
|
|
340
|
+
if (validation.errors.length > 0) {
|
|
341
|
+
console.log(chalk.red('Critical issues found:'));
|
|
342
|
+
validation.errors.forEach((e) => console.log(chalk.red(` • ${e}`)));
|
|
343
|
+
console.log();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (validation.warnings.length > 0) {
|
|
347
|
+
console.log(chalk.yellow('Warnings:'));
|
|
348
|
+
validation.warnings.forEach((w) => console.log(chalk.yellow(` • ${w}`)));
|
|
349
|
+
console.log();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── 7. Start HTTP server ─────────────────────────────────────────────────
|
|
353
|
+
const server = http.createServer((req, res) => {
|
|
354
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
355
|
+
res.end(htmlContent);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
server.listen(port, () => {
|
|
359
|
+
console.log(chalk.gray(`Report written to: ${outputPath}`));
|
|
360
|
+
console.log();
|
|
361
|
+
console.log(chalk.bold.green(`🌐 Server running at: http://localhost:${port}`));
|
|
362
|
+
console.log(chalk.gray('Open the URL in your browser to view the report'));
|
|
363
|
+
console.log(chalk.gray('Press Ctrl+C to stop\n'));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
server.on('error', (err) => {
|
|
367
|
+
if (err.code === 'EADDRINUSE') {
|
|
368
|
+
console.error(chalk.red(`❌ Port ${port} is already in use. Try --port <other-port>`));
|
|
369
|
+
} else {
|
|
370
|
+
console.error(chalk.red('❌ Server error:'), err.message);
|
|
371
|
+
}
|
|
372
|
+
process.exit(1);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
function toPascalCase(str) {
|
|
379
|
+
return str
|
|
380
|
+
.replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase())
|
|
381
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = evaluateSystemCommand;
|