@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 +15 -0
- package/bin/lib/commands/add-dependency.js +734 -0
- package/bin/lib/commands/index.js +2 -0
- package/package.json +1 -1
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
|
};
|