@gxp-dev/tools 2.0.18 → 2.0.19

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/lib/cli.js CHANGED
@@ -23,6 +23,7 @@ const {
23
23
  extensionBuildCommand,
24
24
  extensionInstallCommand,
25
25
  extractConfigCommand,
26
+ addDependencyCommand,
26
27
  } = require("./commands");
27
28
 
28
29
  // Load global configuration
@@ -287,6 +288,20 @@ yargs
287
288
  },
288
289
  extractConfigCommand
289
290
  )
291
+ .command(
292
+ "add-dependency",
293
+ "Add an API dependency to app-manifest.json via interactive wizard",
294
+ {
295
+ env: {
296
+ describe: "API environment to load specs from",
297
+ type: "string",
298
+ default: "staging",
299
+ choices: ["production", "staging", "testing", "develop", "local"],
300
+ alias: "e",
301
+ },
302
+ },
303
+ addDependencyCommand
304
+ )
290
305
  .demandCommand(1, "Please provide a valid command")
291
306
  .help("h")
292
307
  .alias("h", "help")
@@ -0,0 +1,734 @@
1
+ /**
2
+ * Add Dependency Command
3
+ *
4
+ * Interactive wizard for adding API dependencies to app-manifest.json
5
+ * Loads OpenAPI and AsyncAPI specs to help users configure dependencies.
6
+ */
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const https = require("https");
11
+ const http = require("http");
12
+ const { ENVIRONMENT_URLS } = require("../constants");
13
+ const { findProjectRoot } = require("../utils");
14
+
15
+ /**
16
+ * Fetch JSON from a URL
17
+ */
18
+ function fetchJson(url) {
19
+ return new Promise((resolve, reject) => {
20
+ const client = url.startsWith("https") ? https : http;
21
+ const options = {
22
+ rejectUnauthorized: false, // Allow self-signed certs for local dev
23
+ };
24
+
25
+ client
26
+ .get(url, options, (res) => {
27
+ let data = "";
28
+ res.on("data", (chunk) => (data += chunk));
29
+ res.on("end", () => {
30
+ try {
31
+ resolve(JSON.parse(data));
32
+ } catch (e) {
33
+ reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
34
+ }
35
+ });
36
+ })
37
+ .on("error", reject);
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Group OpenAPI paths by their tags
43
+ */
44
+ function groupPathsByTag(openApiSpec) {
45
+ const tagGroups = {};
46
+
47
+ // Initialize tag groups with info from tags array
48
+ if (openApiSpec.tags) {
49
+ for (const tag of openApiSpec.tags) {
50
+ tagGroups[tag.name] = {
51
+ name: tag.name,
52
+ description: tag.description || "",
53
+ paths: [],
54
+ asyncMessages: [],
55
+ };
56
+ }
57
+ }
58
+
59
+ // Group paths by their tags
60
+ for (const [pathUrl, pathMethods] of Object.entries(openApiSpec.paths || {})) {
61
+ for (const [method, pathInfo] of Object.entries(pathMethods)) {
62
+ if (typeof pathInfo !== "object" || !pathInfo.tags) continue;
63
+
64
+ for (const tag of pathInfo.tags) {
65
+ if (!tagGroups[tag]) {
66
+ tagGroups[tag] = {
67
+ name: tag,
68
+ description: "",
69
+ paths: [],
70
+ asyncMessages: [],
71
+ };
72
+ }
73
+
74
+ tagGroups[tag].paths.push({
75
+ path: pathUrl,
76
+ method: method.toUpperCase(),
77
+ operationId: pathInfo.operationId || "",
78
+ summary: pathInfo.summary || "",
79
+ permissions: pathInfo["x-permissions"]?.permissions || [],
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ return tagGroups;
86
+ }
87
+
88
+ /**
89
+ * Extract messages from AsyncAPI and map to tags
90
+ */
91
+ function mapAsyncMessagesToTags(asyncApiSpec, tagGroups) {
92
+ const messages = asyncApiSpec?.components?.messages || {};
93
+
94
+ for (const [messageName, messageInfo] of Object.entries(messages)) {
95
+ // Try to find matching tag based on message name
96
+ // Messages often have format like "GameUpdated", "LeaderboardCreated" etc.
97
+ const baseName = messageName
98
+ .replace(/Created$|Updated$|Deleted$|Changed$|Event$/, "")
99
+ .toLowerCase();
100
+
101
+ for (const [tagName, tagGroup] of Object.entries(tagGroups)) {
102
+ const tagLower = tagName.toLowerCase();
103
+ // Match if tag contains the base name or vice versa
104
+ if (
105
+ tagLower.includes(baseName) ||
106
+ baseName.includes(tagLower) ||
107
+ tagLower === baseName
108
+ ) {
109
+ tagGroup.asyncMessages.push({
110
+ name: messageName,
111
+ description: messageInfo.description || messageInfo.summary || "",
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ return tagGroups;
118
+ }
119
+
120
+ /**
121
+ * Interactive arrow-key selection with type-ahead filtering
122
+ */
123
+ async function selectWithTypeAhead(question, options) {
124
+ return new Promise((resolve) => {
125
+ const stdin = process.stdin;
126
+ const stdout = process.stdout;
127
+
128
+ let selectedIndex = 0;
129
+ let filter = "";
130
+ let filteredOptions = [...options];
131
+
132
+ const applyFilter = () => {
133
+ if (!filter) {
134
+ filteredOptions = [...options];
135
+ } else {
136
+ const lowerFilter = filter.toLowerCase();
137
+ filteredOptions = options.filter(
138
+ (opt) =>
139
+ opt.label.toLowerCase().includes(lowerFilter) ||
140
+ (opt.description && opt.description.toLowerCase().includes(lowerFilter))
141
+ );
142
+ }
143
+ selectedIndex = Math.min(selectedIndex, Math.max(0, filteredOptions.length - 1));
144
+ };
145
+
146
+ const maxVisible = 10;
147
+
148
+ const render = () => {
149
+ stdout.write("\x1B[?25l"); // Hide cursor
150
+
151
+ // Calculate scroll window
152
+ let startIdx = 0;
153
+ if (filteredOptions.length > maxVisible) {
154
+ startIdx = Math.max(0, selectedIndex - Math.floor(maxVisible / 2));
155
+ startIdx = Math.min(startIdx, filteredOptions.length - maxVisible);
156
+ }
157
+ const endIdx = Math.min(startIdx + maxVisible, filteredOptions.length);
158
+ const visibleOptions = filteredOptions.slice(startIdx, endIdx);
159
+
160
+ // Clear screen area
161
+ const linesToClear = maxVisible + 4;
162
+ stdout.write(`\x1B[${linesToClear}A`);
163
+ for (let i = 0; i < linesToClear; i++) {
164
+ stdout.write("\x1B[2K\n");
165
+ }
166
+ stdout.write(`\x1B[${linesToClear}A`);
167
+
168
+ // Print question and filter
169
+ stdout.write(`\x1B[36m?\x1B[0m ${question}\n`);
170
+ stdout.write(` Filter: ${filter}\x1B[90m (type to filter, arrows to navigate)\x1B[0m\n`);
171
+ stdout.write(`\n`);
172
+
173
+ // Print scroll indicator if needed
174
+ if (startIdx > 0) {
175
+ stdout.write(` \x1B[90m↑ ${startIdx} more above\x1B[0m\n`);
176
+ } else {
177
+ stdout.write(`\n`);
178
+ }
179
+
180
+ // Print visible options
181
+ visibleOptions.forEach((opt, i) => {
182
+ const actualIndex = startIdx + i;
183
+ const isSelected = actualIndex === selectedIndex;
184
+ const prefix = isSelected ? "\x1B[36m❯\x1B[0m" : " ";
185
+ const label = isSelected ? `\x1B[36m${opt.label}\x1B[0m` : opt.label;
186
+
187
+ if (opt.description) {
188
+ stdout.write(`${prefix} ${label} \x1B[90m- ${opt.description}\x1B[0m\n`);
189
+ } else {
190
+ stdout.write(`${prefix} ${label}\n`);
191
+ }
192
+ });
193
+
194
+ // Pad remaining lines
195
+ for (let i = visibleOptions.length; i < maxVisible; i++) {
196
+ stdout.write(`\n`);
197
+ }
198
+
199
+ // Print scroll indicator if needed
200
+ if (endIdx < filteredOptions.length) {
201
+ stdout.write(` \x1B[90m↓ ${filteredOptions.length - endIdx} more below\x1B[0m\n`);
202
+ } else {
203
+ stdout.write(`\n`);
204
+ }
205
+
206
+ stdout.write(`\x1B[?25h`); // Show cursor
207
+ };
208
+
209
+ const cleanup = () => {
210
+ stdin.setRawMode(false);
211
+ stdin.removeAllListeners("data");
212
+ stdin.pause();
213
+ // Clear UI
214
+ const linesToClear = maxVisible + 4;
215
+ stdout.write(`\x1B[${linesToClear}A`);
216
+ for (let i = 0; i < linesToClear; i++) {
217
+ stdout.write("\x1B[2K\n");
218
+ }
219
+ stdout.write(`\x1B[${linesToClear}A`);
220
+ };
221
+
222
+ // Initial render with spacing
223
+ console.log("");
224
+ for (let i = 0; i < maxVisible + 3; i++) {
225
+ console.log("");
226
+ }
227
+
228
+ stdin.setRawMode(true);
229
+ stdin.resume();
230
+ stdin.setEncoding("utf8");
231
+
232
+ let buffer = "";
233
+ stdin.on("data", (data) => {
234
+ buffer += data;
235
+
236
+ while (buffer.length > 0) {
237
+ // Ctrl+C
238
+ if (buffer[0] === "\x03") {
239
+ cleanup();
240
+ process.exit(0);
241
+ }
242
+
243
+ // Enter
244
+ if (buffer[0] === "\r" || buffer[0] === "\n") {
245
+ cleanup();
246
+ const selected = filteredOptions[selectedIndex];
247
+ if (selected) {
248
+ stdout.write(`\x1B[36m?\x1B[0m ${question} \x1B[36m${selected.label}\x1B[0m\n`);
249
+ resolve(selected.value);
250
+ } else {
251
+ resolve(null);
252
+ }
253
+ buffer = buffer.slice(1);
254
+ return;
255
+ }
256
+
257
+ // Escape sequences
258
+ if (buffer.startsWith("\x1b[A") || buffer.startsWith("\x1bOA")) {
259
+ // Up arrow
260
+ selectedIndex = Math.max(0, selectedIndex - 1);
261
+ render();
262
+ buffer = buffer.slice(3);
263
+ continue;
264
+ }
265
+ if (buffer.startsWith("\x1b[B") || buffer.startsWith("\x1bOB")) {
266
+ // Down arrow
267
+ selectedIndex = Math.min(filteredOptions.length - 1, selectedIndex + 1);
268
+ render();
269
+ buffer = buffer.slice(3);
270
+ continue;
271
+ }
272
+ if (buffer.startsWith("\x1b[") || buffer.startsWith("\x1bO")) {
273
+ if (buffer.length >= 3) {
274
+ buffer = buffer.slice(3);
275
+ continue;
276
+ }
277
+ break;
278
+ }
279
+ if (buffer.startsWith("\x1b")) {
280
+ if (buffer.length >= 2) {
281
+ buffer = buffer.slice(1);
282
+ continue;
283
+ }
284
+ break;
285
+ }
286
+
287
+ // Backspace
288
+ if (buffer[0] === "\x7f" || buffer[0] === "\b") {
289
+ filter = filter.slice(0, -1);
290
+ applyFilter();
291
+ render();
292
+ buffer = buffer.slice(1);
293
+ continue;
294
+ }
295
+
296
+ // Regular character
297
+ if (buffer[0].charCodeAt(0) >= 32 && buffer[0].charCodeAt(0) < 127) {
298
+ filter += buffer[0];
299
+ applyFilter();
300
+ render();
301
+ }
302
+ buffer = buffer.slice(1);
303
+ }
304
+ });
305
+
306
+ render();
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Multi-select with spacebar toggle
312
+ */
313
+ async function multiSelectPrompt(question, options) {
314
+ return new Promise((resolve) => {
315
+ const stdin = process.stdin;
316
+ const stdout = process.stdout;
317
+
318
+ let selectedIndex = 0;
319
+ const selected = new Set();
320
+ const maxVisible = 10;
321
+
322
+ const render = () => {
323
+ stdout.write("\x1B[?25l");
324
+
325
+ // Calculate scroll window
326
+ let startIdx = 0;
327
+ if (options.length > maxVisible) {
328
+ startIdx = Math.max(0, selectedIndex - Math.floor(maxVisible / 2));
329
+ startIdx = Math.min(startIdx, options.length - maxVisible);
330
+ }
331
+ const endIdx = Math.min(startIdx + maxVisible, options.length);
332
+ const visibleOptions = options.slice(startIdx, endIdx);
333
+
334
+ const linesToClear = maxVisible + 5;
335
+ stdout.write(`\x1B[${linesToClear}A`);
336
+ for (let i = 0; i < linesToClear; i++) {
337
+ stdout.write("\x1B[2K\n");
338
+ }
339
+ stdout.write(`\x1B[${linesToClear}A`);
340
+
341
+ stdout.write(`\x1B[36m?\x1B[0m ${question}\n`);
342
+ stdout.write(` \x1B[90m(Space to toggle, Enter to confirm, A to toggle all)\x1B[0m\n`);
343
+ stdout.write(` Selected: ${selected.size} of ${options.length}\n`);
344
+
345
+ if (startIdx > 0) {
346
+ stdout.write(` \x1B[90m↑ ${startIdx} more above\x1B[0m\n`);
347
+ } else {
348
+ stdout.write(`\n`);
349
+ }
350
+
351
+ visibleOptions.forEach((opt, i) => {
352
+ const actualIndex = startIdx + i;
353
+ const isHighlighted = actualIndex === selectedIndex;
354
+ const isSelected = selected.has(actualIndex);
355
+
356
+ const checkbox = isSelected ? "\x1B[32m◉\x1B[0m" : "○";
357
+ const prefix = isHighlighted ? "\x1B[36m❯\x1B[0m" : " ";
358
+ const label = isHighlighted ? `\x1B[36m${opt.label}\x1B[0m` : opt.label;
359
+
360
+ if (opt.description) {
361
+ stdout.write(`${prefix} ${checkbox} ${label} \x1B[90m- ${opt.description}\x1B[0m\n`);
362
+ } else {
363
+ stdout.write(`${prefix} ${checkbox} ${label}\n`);
364
+ }
365
+ });
366
+
367
+ for (let i = visibleOptions.length; i < maxVisible; i++) {
368
+ stdout.write(`\n`);
369
+ }
370
+
371
+ if (endIdx < options.length) {
372
+ stdout.write(` \x1B[90m↓ ${options.length - endIdx} more below\x1B[0m\n`);
373
+ } else {
374
+ stdout.write(`\n`);
375
+ }
376
+
377
+ stdout.write(`\x1B[?25h`);
378
+ };
379
+
380
+ const cleanup = () => {
381
+ stdin.setRawMode(false);
382
+ stdin.removeAllListeners("data");
383
+ stdin.pause();
384
+ const linesToClear = maxVisible + 5;
385
+ stdout.write(`\x1B[${linesToClear}A`);
386
+ for (let i = 0; i < linesToClear; i++) {
387
+ stdout.write("\x1B[2K\n");
388
+ }
389
+ stdout.write(`\x1B[${linesToClear}A`);
390
+ };
391
+
392
+ console.log("");
393
+ for (let i = 0; i < maxVisible + 4; i++) {
394
+ console.log("");
395
+ }
396
+
397
+ stdin.setRawMode(true);
398
+ stdin.resume();
399
+ stdin.setEncoding("utf8");
400
+
401
+ let buffer = "";
402
+ stdin.on("data", (data) => {
403
+ buffer += data;
404
+
405
+ while (buffer.length > 0) {
406
+ if (buffer[0] === "\x03") {
407
+ cleanup();
408
+ process.exit(0);
409
+ }
410
+
411
+ if (buffer[0] === "\r" || buffer[0] === "\n") {
412
+ cleanup();
413
+ const selectedOptions = options.filter((_, i) => selected.has(i));
414
+ stdout.write(
415
+ `\x1B[36m?\x1B[0m ${question} \x1B[36m${selectedOptions.length} selected\x1B[0m\n`
416
+ );
417
+ resolve(selectedOptions.map((opt) => opt.value));
418
+ buffer = buffer.slice(1);
419
+ return;
420
+ }
421
+
422
+ if (buffer.startsWith("\x1b[A") || buffer.startsWith("\x1bOA")) {
423
+ selectedIndex = Math.max(0, selectedIndex - 1);
424
+ render();
425
+ buffer = buffer.slice(3);
426
+ continue;
427
+ }
428
+ if (buffer.startsWith("\x1b[B") || buffer.startsWith("\x1bOB")) {
429
+ selectedIndex = Math.min(options.length - 1, selectedIndex + 1);
430
+ render();
431
+ buffer = buffer.slice(3);
432
+ continue;
433
+ }
434
+ if (buffer.startsWith("\x1b[") || buffer.startsWith("\x1bO")) {
435
+ if (buffer.length >= 3) {
436
+ buffer = buffer.slice(3);
437
+ continue;
438
+ }
439
+ break;
440
+ }
441
+ if (buffer.startsWith("\x1b")) {
442
+ if (buffer.length >= 2) {
443
+ buffer = buffer.slice(1);
444
+ continue;
445
+ }
446
+ break;
447
+ }
448
+
449
+ // Spacebar toggle
450
+ if (buffer[0] === " ") {
451
+ if (selected.has(selectedIndex)) {
452
+ selected.delete(selectedIndex);
453
+ } else {
454
+ selected.add(selectedIndex);
455
+ }
456
+ render();
457
+ buffer = buffer.slice(1);
458
+ continue;
459
+ }
460
+
461
+ // 'a' or 'A' toggle all
462
+ if (buffer[0] === "a" || buffer[0] === "A") {
463
+ if (selected.size === options.length) {
464
+ selected.clear();
465
+ } else {
466
+ options.forEach((_, i) => selected.add(i));
467
+ }
468
+ render();
469
+ buffer = buffer.slice(1);
470
+ continue;
471
+ }
472
+
473
+ buffer = buffer.slice(1);
474
+ }
475
+ });
476
+
477
+ render();
478
+ });
479
+ }
480
+
481
+ /**
482
+ * Text input with prepopulated value
483
+ */
484
+ async function textInput(question, defaultValue = "") {
485
+ return new Promise((resolve) => {
486
+ const stdin = process.stdin;
487
+ const stdout = process.stdout;
488
+
489
+ let value = defaultValue;
490
+ let cursorPos = value.length;
491
+
492
+ const render = () => {
493
+ stdout.write("\x1B[?25l");
494
+ stdout.write("\r\x1B[2K");
495
+ stdout.write(`\x1B[36m?\x1B[0m ${question}: ${value}`);
496
+ const totalLength = question.length + 4 + cursorPos;
497
+ stdout.write(`\r\x1B[${totalLength}C`);
498
+ stdout.write("\x1B[?25h");
499
+ };
500
+
501
+ const cleanup = () => {
502
+ stdin.setRawMode(false);
503
+ stdin.removeAllListeners("data");
504
+ stdin.pause();
505
+ };
506
+
507
+ console.log("");
508
+ render();
509
+
510
+ stdin.setRawMode(true);
511
+ stdin.resume();
512
+ stdin.setEncoding("utf8");
513
+
514
+ stdin.on("data", (key) => {
515
+ if (key === "\x03") {
516
+ cleanup();
517
+ process.exit(0);
518
+ }
519
+
520
+ if (key === "\r" || key === "\n") {
521
+ cleanup();
522
+ stdout.write("\r\x1B[2K");
523
+ stdout.write(`\x1B[36m?\x1B[0m ${question}: \x1B[36m${value}\x1B[0m\n`);
524
+ resolve(value);
525
+ return;
526
+ }
527
+
528
+ if (key === "\x7f" || key === "\b") {
529
+ if (cursorPos > 0) {
530
+ value = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
531
+ cursorPos--;
532
+ }
533
+ render();
534
+ return;
535
+ }
536
+
537
+ if (key === "\x1b[D") {
538
+ cursorPos = Math.max(0, cursorPos - 1);
539
+ render();
540
+ return;
541
+ }
542
+
543
+ if (key === "\x1b[C") {
544
+ cursorPos = Math.min(value.length, cursorPos + 1);
545
+ render();
546
+ return;
547
+ }
548
+
549
+ if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
550
+ value = value.slice(0, cursorPos) + key + value.slice(cursorPos);
551
+ cursorPos++;
552
+ render();
553
+ }
554
+ });
555
+ });
556
+ }
557
+
558
+ /**
559
+ * Main command handler
560
+ */
561
+ async function addDependencyCommand(argv) {
562
+ const environment = argv.env || "staging";
563
+
564
+ console.log("");
565
+ console.log("\x1B[36m╔════════════════════════════════════════════╗\x1B[0m");
566
+ console.log("\x1B[36m║ Add API Dependency Wizard ║\x1B[0m");
567
+ console.log("\x1B[36m╚════════════════════════════════════════════╝\x1B[0m");
568
+ console.log("");
569
+
570
+ // Get environment URLs
571
+ const envUrls = ENVIRONMENT_URLS[environment];
572
+ if (!envUrls) {
573
+ console.error(`\x1B[31m✗ Unknown environment: ${environment}\x1B[0m`);
574
+ console.log(` Available: ${Object.keys(ENVIRONMENT_URLS).join(", ")}`);
575
+ process.exit(1);
576
+ }
577
+
578
+ console.log(`\x1B[90mEnvironment: ${environment}\x1B[0m`);
579
+ console.log(`\x1B[90mLoading API specifications...\x1B[0m`);
580
+
581
+ // Fetch specs
582
+ let openApiSpec, asyncApiSpec;
583
+ try {
584
+ [openApiSpec, asyncApiSpec] = await Promise.all([
585
+ fetchJson(envUrls.openApiSpec),
586
+ fetchJson(envUrls.asyncApiSpec).catch(() => ({ components: { messages: {} } })),
587
+ ]);
588
+ } catch (error) {
589
+ console.error(`\x1B[31m✗ Failed to load API specs: ${error.message}\x1B[0m`);
590
+ process.exit(1);
591
+ }
592
+
593
+ console.log(`\x1B[32m✓ Loaded OpenAPI spec\x1B[0m`);
594
+ console.log(`\x1B[32m✓ Loaded AsyncAPI spec\x1B[0m`);
595
+ console.log("");
596
+
597
+ // Group paths by tags and map async messages
598
+ let tagGroups = groupPathsByTag(openApiSpec);
599
+ tagGroups = mapAsyncMessagesToTags(asyncApiSpec, tagGroups);
600
+
601
+ // Filter out empty tags
602
+ const tags = Object.values(tagGroups)
603
+ .filter((t) => t.paths.length > 0)
604
+ .sort((a, b) => a.name.localeCompare(b.name));
605
+
606
+ if (tags.length === 0) {
607
+ console.error("\x1B[31m✗ No API tags found in spec\x1B[0m");
608
+ process.exit(1);
609
+ }
610
+
611
+ // Step 1: Select a tag
612
+ const tagOptions = tags.map((t) => ({
613
+ label: t.name,
614
+ description: `${t.paths.length} endpoints${t.asyncMessages.length > 0 ? `, ${t.asyncMessages.length} events` : ""}`,
615
+ value: t,
616
+ }));
617
+
618
+ const selectedTag = await selectWithTypeAhead("Select an API model (tag):", tagOptions);
619
+
620
+ if (!selectedTag) {
621
+ console.log("Cancelled.");
622
+ process.exit(0);
623
+ }
624
+
625
+ console.log("");
626
+
627
+ // Step 2: Select paths
628
+ const pathOptions = selectedTag.paths.map((p) => ({
629
+ label: `${p.method} ${p.path}`,
630
+ description: p.summary,
631
+ value: p,
632
+ }));
633
+
634
+ const selectedPaths = await multiSelectPrompt(
635
+ `Select API endpoints for ${selectedTag.name}:`,
636
+ pathOptions
637
+ );
638
+
639
+ if (selectedPaths.length === 0) {
640
+ console.log("\x1B[33m⚠ No endpoints selected. Using all endpoints.\x1B[0m");
641
+ selectedPaths.push(...selectedTag.paths);
642
+ }
643
+
644
+ console.log("");
645
+
646
+ // Step 3: Select async messages (if any)
647
+ let selectedMessages = [];
648
+ if (selectedTag.asyncMessages.length > 0) {
649
+ const messageOptions = selectedTag.asyncMessages.map((m) => ({
650
+ label: m.name,
651
+ description: m.description,
652
+ value: m,
653
+ }));
654
+
655
+ selectedMessages = await multiSelectPrompt(
656
+ `Select socket events for ${selectedTag.name}:`,
657
+ messageOptions
658
+ );
659
+ console.log("");
660
+ }
661
+
662
+ // Step 4: Enter identifier
663
+ const defaultIdentifier = selectedTag.name
664
+ .toLowerCase()
665
+ .replace(/[^a-z0-9]+/g, "_")
666
+ .replace(/^_|_$/g, "");
667
+
668
+ const identifier = await textInput("Enter dependency identifier:", defaultIdentifier);
669
+
670
+ // Collect all permissions from selected paths
671
+ const allPermissions = new Set();
672
+ for (const pathInfo of selectedPaths) {
673
+ for (const perm of pathInfo.permissions || []) {
674
+ allPermissions.add(perm);
675
+ }
676
+ }
677
+
678
+ // Build events object
679
+ const events = {};
680
+ for (const msg of selectedMessages) {
681
+ events[msg.name] = msg.name;
682
+ }
683
+
684
+ // Build the dependency object
685
+ const dependency = {
686
+ identifier,
687
+ model: selectedTag.name,
688
+ permissions: Array.from(allPermissions).sort(),
689
+ events,
690
+ };
691
+
692
+ console.log("");
693
+ console.log("\x1B[36m─────────────────────────────────────────────\x1B[0m");
694
+ console.log("\x1B[36mGenerated Dependency Configuration:\x1B[0m");
695
+ console.log("\x1B[36m─────────────────────────────────────────────\x1B[0m");
696
+ console.log(JSON.stringify(dependency, null, 2));
697
+ console.log("");
698
+
699
+ // Find and update app-manifest.json
700
+ const projectPath = findProjectRoot();
701
+ const manifestPath = path.join(projectPath, "app-manifest.json");
702
+
703
+ if (!fs.existsSync(manifestPath)) {
704
+ console.log("\x1B[33m⚠ app-manifest.json not found. Creating new file.\x1B[0m");
705
+ const manifest = { dependencies: [dependency] };
706
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
707
+ console.log(`\x1B[32m✓ Created app-manifest.json with dependency\x1B[0m`);
708
+ } else {
709
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
710
+ manifest.dependencies = manifest.dependencies || [];
711
+
712
+ // Check for existing dependency with same identifier
713
+ const existingIndex = manifest.dependencies.findIndex(
714
+ (d) => d.identifier === identifier
715
+ );
716
+
717
+ if (existingIndex >= 0) {
718
+ manifest.dependencies[existingIndex] = dependency;
719
+ console.log(`\x1B[32m✓ Updated existing dependency: ${identifier}\x1B[0m`);
720
+ } else {
721
+ manifest.dependencies.push(dependency);
722
+ console.log(`\x1B[32m✓ Added new dependency: ${identifier}\x1B[0m`);
723
+ }
724
+
725
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
726
+ }
727
+
728
+ console.log(`\x1B[32m✓ Saved to ${manifestPath}\x1B[0m`);
729
+ console.log("");
730
+ }
731
+
732
+ module.exports = {
733
+ addDependencyCommand,
734
+ };
@@ -19,6 +19,7 @@ const {
19
19
  extensionInstallCommand,
20
20
  } = require("./extensions");
21
21
  const { extractConfigCommand } = require("./extract-config");
22
+ const { addDependencyCommand } = require("./add-dependency");
22
23
 
23
24
  module.exports = {
24
25
  initCommand,
@@ -34,4 +35,5 @@ module.exports = {
34
35
  extensionBuildCommand,
35
36
  extensionInstallCommand,
36
37
  extractConfigCommand,
38
+ addDependencyCommand,
37
39
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {