@vrdmr/fnx-test 0.4.2 → 0.5.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/lib/azurite-manager.js +67 -9
- package/lib/chat/index.js +281 -0
- package/lib/cli.js +36 -0
- package/lib/config.js +77 -23
- package/lib/host-launcher.js +121 -30
- package/lib/init/manifest.js +250 -0
- package/lib/init/prompts.js +673 -0
- package/lib/init/scaffold.js +689 -0
- package/lib/init.js +540 -0
- package/lib/runtimes.js +238 -0
- package/lib/setup/agent-detect.js +92 -0
- package/lib/setup/detect.js +117 -0
- package/lib/setup/index.js +572 -0
- package/lib/utils.js +27 -0
- package/manifests/default.yaml +124 -0
- package/manifests/skills/fnx-best-practices/SKILL.md +64 -0
- package/manifests/skills/fnx-best-practices/references/azure-functions-docs.md +60 -0
- package/manifests/skills/fnx-best-practices/references/fnx-specific.md +97 -0
- package/manifests/skills/fnx-create-function/SKILL.md +133 -0
- package/manifests/skills/fnx-create-function/references/templates.md +105 -0
- package/manifests/skills/fnx-diagnostics/SKILL.md +84 -0
- package/manifests/skills/fnx-diagnostics/references/diagnostic-checklist.md +59 -0
- package/manifests/skills/fnx-diagnostics/references/fnx-error-patterns.md +71 -0
- package/manifests/skills/fnx-feedback/SKILL.md +142 -0
- package/manifests/skills/fnx-intro/SKILL.md +83 -0
- package/manifests/skills/fnx-intro/references/fnx-commands.md +157 -0
- package/manifests/skills/fnx-intro/references/roadmap.md +60 -0
- package/package.json +3 -1
- package/profiles/sku-profiles.json +6 -6
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for fnx init
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js readline for cross-platform terminal interaction.
|
|
5
|
+
* No external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { success, dim, bold, funcName } from '../colors.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a readline interface for prompts
|
|
13
|
+
*/
|
|
14
|
+
function createPrompt() {
|
|
15
|
+
return createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if we can use raw mode (interactive terminal)
|
|
23
|
+
*/
|
|
24
|
+
function canUseRawMode() {
|
|
25
|
+
return process.stdin.isTTY && typeof process.stdin.setRawMode === 'function';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clean up stdin after raw mode usage to allow process to exit
|
|
30
|
+
*/
|
|
31
|
+
function cleanupStdin(stdin, listener) {
|
|
32
|
+
stdin.setRawMode(false);
|
|
33
|
+
stdin.removeListener('data', listener);
|
|
34
|
+
stdin.pause();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prompt user to select from a list of options using arrow keys
|
|
39
|
+
* Falls back to number input if raw mode is not available
|
|
40
|
+
* @param {string} question - Question to display
|
|
41
|
+
* @param {Array<{value: any, label: string}>} options - Available options
|
|
42
|
+
* @returns {Promise<any>} Selected value
|
|
43
|
+
*/
|
|
44
|
+
async function selectPrompt(question, options) {
|
|
45
|
+
// Fall back to number input if raw mode not available (CI, piped input)
|
|
46
|
+
if (!canUseRawMode()) {
|
|
47
|
+
return selectPromptNumbered(question, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
// Find first selectable index
|
|
52
|
+
let selectedIndex = options.findIndex(o => !o.disabled);
|
|
53
|
+
if (selectedIndex === -1) selectedIndex = 0;
|
|
54
|
+
|
|
55
|
+
const stdin = process.stdin;
|
|
56
|
+
|
|
57
|
+
// Find next/prev selectable index (skipping disabled)
|
|
58
|
+
const findNextSelectable = (from, direction) => {
|
|
59
|
+
let idx = from;
|
|
60
|
+
for (let i = 0; i < options.length; i++) {
|
|
61
|
+
idx = (idx + direction + options.length) % options.length;
|
|
62
|
+
if (!options[idx].disabled) return idx;
|
|
63
|
+
}
|
|
64
|
+
return from; // No selectable found, stay in place
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Track total lines rendered (options + hint)
|
|
68
|
+
let totalLines = 0;
|
|
69
|
+
|
|
70
|
+
// Render the menu
|
|
71
|
+
const render = (isFirstRender = false) => {
|
|
72
|
+
// Move cursor up to overwrite previous render (except first render)
|
|
73
|
+
if (!isFirstRender && totalLines > 0) {
|
|
74
|
+
process.stdout.write(`\x1b[${totalLines}A`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let lines = 0;
|
|
78
|
+
for (let i = 0; i < options.length; i++) {
|
|
79
|
+
const opt = options[i];
|
|
80
|
+
if (opt.disabled) {
|
|
81
|
+
// Show separator/disabled items without selection indicator
|
|
82
|
+
process.stdout.write(`\x1b[2K ${opt.label}\n`);
|
|
83
|
+
} else {
|
|
84
|
+
const prefix = i === selectedIndex ? success('❯') : ' ';
|
|
85
|
+
const label = i === selectedIndex ? bold(opt.label) : opt.label;
|
|
86
|
+
process.stdout.write(`\x1b[2K ${prefix} ${label}\n`);
|
|
87
|
+
}
|
|
88
|
+
lines++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Show hint on first render, clear and rewrite on subsequent
|
|
92
|
+
process.stdout.write(`\x1b[2K${dim(' ↑/↓ to move, Enter to select')}\n`);
|
|
93
|
+
lines++;
|
|
94
|
+
|
|
95
|
+
totalLines = lines;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Show question and initial render
|
|
99
|
+
console.log(bold(question));
|
|
100
|
+
render(true);
|
|
101
|
+
|
|
102
|
+
// Enable raw mode for keypress detection
|
|
103
|
+
stdin.setRawMode(true);
|
|
104
|
+
stdin.resume();
|
|
105
|
+
stdin.setEncoding('utf8');
|
|
106
|
+
|
|
107
|
+
const onKeypress = (key) => {
|
|
108
|
+
// Handle Ctrl+C
|
|
109
|
+
if (key === '\x03') {
|
|
110
|
+
cleanupStdin(stdin, onKeypress);
|
|
111
|
+
console.log('\n');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle arrow keys (escape sequences)
|
|
116
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
117
|
+
// Up arrow or k - find previous selectable
|
|
118
|
+
selectedIndex = findNextSelectable(selectedIndex, -1);
|
|
119
|
+
render();
|
|
120
|
+
} else if (key === '\x1b[B' || key === 'j') {
|
|
121
|
+
// Down arrow or j - find next selectable
|
|
122
|
+
selectedIndex = findNextSelectable(selectedIndex, 1);
|
|
123
|
+
render();
|
|
124
|
+
} else if (key === '\r' || key === '\n') {
|
|
125
|
+
// Enter - only if current option is selectable
|
|
126
|
+
if (!options[selectedIndex].disabled) {
|
|
127
|
+
cleanupStdin(stdin, onKeypress);
|
|
128
|
+
process.stdout.write(`\x1b[2K`);
|
|
129
|
+
console.log(success(` ✓ ${options[selectedIndex].label}\n`));
|
|
130
|
+
resolve(options[selectedIndex].value);
|
|
131
|
+
}
|
|
132
|
+
} else if (key >= '1' && key <= '9') {
|
|
133
|
+
// Number keys - count only selectable options
|
|
134
|
+
const num = parseInt(key, 10);
|
|
135
|
+
const selectableOptions = options.filter(o => !o.disabled);
|
|
136
|
+
if (num >= 1 && num <= selectableOptions.length) {
|
|
137
|
+
// Find the actual index of the nth selectable option
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (let i = 0; i < options.length; i++) {
|
|
140
|
+
if (!options[i].disabled) {
|
|
141
|
+
count++;
|
|
142
|
+
if (count === num) {
|
|
143
|
+
cleanupStdin(stdin, onKeypress);
|
|
144
|
+
process.stdout.write(`\x1b[2K`);
|
|
145
|
+
console.log(success(` ✓ ${options[i].label}\n`));
|
|
146
|
+
resolve(options[i].value);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
stdin.on('data', onKeypress);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Prompt with search capability (type to filter)
|
|
161
|
+
* Used for large lists like templates where search helps navigation
|
|
162
|
+
* @param {string} question - Question to display
|
|
163
|
+
* @param {Array<{value: any, label: string, searchText?: string}>} allOptions - All options
|
|
164
|
+
* @returns {Promise<any>} Selected value
|
|
165
|
+
*/
|
|
166
|
+
async function selectPromptWithSearch(question, allOptions) {
|
|
167
|
+
// Fall back to number input if raw mode not available
|
|
168
|
+
if (!canUseRawMode()) {
|
|
169
|
+
return selectPromptNumbered(question, allOptions);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const MIN_SEARCH_LENGTH = 3;
|
|
173
|
+
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
let searchQuery = '';
|
|
176
|
+
let displayOptions = allOptions;
|
|
177
|
+
let selectedIndex = displayOptions.findIndex(o => !o.disabled);
|
|
178
|
+
if (selectedIndex === -1) selectedIndex = 0;
|
|
179
|
+
|
|
180
|
+
const stdin = process.stdin;
|
|
181
|
+
|
|
182
|
+
// Filter options based on search query
|
|
183
|
+
const filterOptions = () => {
|
|
184
|
+
if (searchQuery.length >= MIN_SEARCH_LENGTH) {
|
|
185
|
+
const query = searchQuery.toLowerCase();
|
|
186
|
+
displayOptions = allOptions.filter(opt => {
|
|
187
|
+
if (opt.disabled) return false;
|
|
188
|
+
const label = (opt.label || '').toLowerCase();
|
|
189
|
+
const searchText = (opt.searchText || '').toLowerCase();
|
|
190
|
+
return label.includes(query) || searchText.includes(query);
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
displayOptions = allOptions;
|
|
194
|
+
}
|
|
195
|
+
// Reset selection to first visible item
|
|
196
|
+
selectedIndex = displayOptions.findIndex(o => !o.disabled);
|
|
197
|
+
if (selectedIndex === -1) selectedIndex = 0;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Find next/prev selectable index
|
|
201
|
+
const findNextSelectable = (from, direction) => {
|
|
202
|
+
let idx = from;
|
|
203
|
+
for (let i = 0; i < displayOptions.length; i++) {
|
|
204
|
+
idx = (idx + direction + displayOptions.length) % displayOptions.length;
|
|
205
|
+
if (!displayOptions[idx].disabled) return idx;
|
|
206
|
+
}
|
|
207
|
+
return from;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Calculate the height of the display area
|
|
211
|
+
let lastRenderHeight = 0;
|
|
212
|
+
|
|
213
|
+
// Render the menu
|
|
214
|
+
const render = (isFirstRender = false) => {
|
|
215
|
+
// Clear previous render
|
|
216
|
+
if (!isFirstRender && lastRenderHeight > 0) {
|
|
217
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
218
|
+
for (let i = 0; i < lastRenderHeight; i++) {
|
|
219
|
+
process.stdout.write(`\x1b[2K\n`);
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Show search query if typing
|
|
225
|
+
let lines = 0;
|
|
226
|
+
if (searchQuery.length > 0) {
|
|
227
|
+
process.stdout.write(`\x1b[2K ${dim('Filter:')} ${searchQuery}\n`);
|
|
228
|
+
lines++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Show filtered results
|
|
232
|
+
if (displayOptions.length === 0) {
|
|
233
|
+
process.stdout.write(`\x1b[2K ${dim('No matches found')}\n`);
|
|
234
|
+
lines++;
|
|
235
|
+
} else {
|
|
236
|
+
for (let i = 0; i < displayOptions.length; i++) {
|
|
237
|
+
const opt = displayOptions[i];
|
|
238
|
+
if (opt.disabled) {
|
|
239
|
+
process.stdout.write(`\x1b[2K ${opt.label}\n`);
|
|
240
|
+
} else {
|
|
241
|
+
const prefix = i === selectedIndex ? success('❯') : ' ';
|
|
242
|
+
const label = i === selectedIndex ? bold(opt.label) : opt.label;
|
|
243
|
+
process.stdout.write(`\x1b[2K ${prefix} ${label}\n`);
|
|
244
|
+
}
|
|
245
|
+
lines++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Show hint as part of render
|
|
250
|
+
const hintText = '↑/↓ to move, type to filter, Esc to clear, Enter to select';
|
|
251
|
+
process.stdout.write(`\x1b[2K${dim(' ' + hintText)}\n`);
|
|
252
|
+
lines++;
|
|
253
|
+
|
|
254
|
+
lastRenderHeight = lines;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Show question and initial render
|
|
258
|
+
console.log(bold(question));
|
|
259
|
+
render(true);
|
|
260
|
+
|
|
261
|
+
stdin.setRawMode(true);
|
|
262
|
+
stdin.resume();
|
|
263
|
+
stdin.setEncoding('utf8');
|
|
264
|
+
|
|
265
|
+
const onKeypress = (key) => {
|
|
266
|
+
// Ctrl+C
|
|
267
|
+
if (key === '\x03') {
|
|
268
|
+
cleanupStdin(stdin, onKeypress);
|
|
269
|
+
console.log('\n');
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Escape - clear search
|
|
274
|
+
if (key === '\x1b' && key.length === 1) {
|
|
275
|
+
searchQuery = '';
|
|
276
|
+
filterOptions();
|
|
277
|
+
render();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Up arrow (j/k only when not searching - they're valid search chars)
|
|
282
|
+
if (key === '\x1b[A' || (searchQuery.length === 0 && key === 'k')) {
|
|
283
|
+
selectedIndex = findNextSelectable(selectedIndex, -1);
|
|
284
|
+
render();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Down arrow (j/k only when not searching - they're valid search chars)
|
|
289
|
+
if (key === '\x1b[B' || (searchQuery.length === 0 && key === 'j')) {
|
|
290
|
+
selectedIndex = findNextSelectable(selectedIndex, 1);
|
|
291
|
+
render();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Enter
|
|
296
|
+
if (key === '\r' || key === '\n') {
|
|
297
|
+
if (displayOptions.length > 0 && !displayOptions[selectedIndex].disabled) {
|
|
298
|
+
cleanupStdin(stdin, onKeypress);
|
|
299
|
+
process.stdout.write(`\x1b[2K`);
|
|
300
|
+
console.log(success(` ✓ ${displayOptions[selectedIndex].label}\n`));
|
|
301
|
+
resolve(displayOptions[selectedIndex].value);
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Backspace - remove last char from search
|
|
307
|
+
if (key === '\x7f' || key === '\x08') {
|
|
308
|
+
if (searchQuery.length > 0) {
|
|
309
|
+
searchQuery = searchQuery.slice(0, -1);
|
|
310
|
+
filterOptions();
|
|
311
|
+
render();
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Number keys 1-9 for quick selection (only when not searching)
|
|
317
|
+
if (searchQuery.length === 0 && key >= '1' && key <= '9') {
|
|
318
|
+
const num = parseInt(key, 10);
|
|
319
|
+
const selectableOptions = displayOptions.filter(o => !o.disabled);
|
|
320
|
+
if (num >= 1 && num <= selectableOptions.length) {
|
|
321
|
+
let count = 0;
|
|
322
|
+
for (let i = 0; i < displayOptions.length; i++) {
|
|
323
|
+
if (!displayOptions[i].disabled) {
|
|
324
|
+
count++;
|
|
325
|
+
if (count === num) {
|
|
326
|
+
cleanupStdin(stdin, onKeypress);
|
|
327
|
+
process.stdout.write(`\x1b[2K`);
|
|
328
|
+
console.log(success(` ✓ ${displayOptions[i].label}\n`));
|
|
329
|
+
resolve(displayOptions[i].value);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Printable characters - add to search
|
|
338
|
+
if (key.length === 1 && key >= ' ' && key <= '~') {
|
|
339
|
+
searchQuery += key;
|
|
340
|
+
filterOptions();
|
|
341
|
+
render();
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
stdin.on('data', onKeypress);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Fallback: Prompt user to select using number input
|
|
351
|
+
* Used when raw mode is not available (CI, piped input)
|
|
352
|
+
*/
|
|
353
|
+
async function selectPromptNumbered(question, options) {
|
|
354
|
+
const rl = createPrompt();
|
|
355
|
+
|
|
356
|
+
rl.on('error', () => {
|
|
357
|
+
rl.close();
|
|
358
|
+
process.exit(1);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Filter out disabled options and build mapping
|
|
362
|
+
const selectableOptions = [];
|
|
363
|
+
const indexMap = {}; // maps displayed number -> original index
|
|
364
|
+
|
|
365
|
+
console.log(bold(question));
|
|
366
|
+
let displayNum = 0;
|
|
367
|
+
options.forEach((opt, originalIdx) => {
|
|
368
|
+
if (opt.disabled) {
|
|
369
|
+
// Show separator without number
|
|
370
|
+
console.log(` ${opt.label}`);
|
|
371
|
+
} else {
|
|
372
|
+
displayNum++;
|
|
373
|
+
selectableOptions.push(opt);
|
|
374
|
+
indexMap[displayNum] = originalIdx;
|
|
375
|
+
console.log(` ${dim(`[${displayNum}]`)} ${opt.label}`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return new Promise((resolve) => {
|
|
380
|
+
const ask = () => {
|
|
381
|
+
rl.question(`\n ${dim('Enter number (1-' + selectableOptions.length + '):')} `, (answer) => {
|
|
382
|
+
const num = parseInt(answer.trim(), 10);
|
|
383
|
+
if (num >= 1 && num <= selectableOptions.length) {
|
|
384
|
+
rl.close();
|
|
385
|
+
console.log(success(` ✓ ${selectableOptions[num - 1].label}\n`));
|
|
386
|
+
resolve(selectableOptions[num - 1].value);
|
|
387
|
+
} else {
|
|
388
|
+
console.log(dim(' Invalid selection, try again.'));
|
|
389
|
+
ask();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
ask();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Prompt for text input
|
|
399
|
+
* @param {string} question - Question to display
|
|
400
|
+
* @param {string} defaultValue - Default value if empty
|
|
401
|
+
* @returns {Promise<string>} User input
|
|
402
|
+
*/
|
|
403
|
+
async function textPrompt(question, defaultValue = '') {
|
|
404
|
+
const rl = createPrompt();
|
|
405
|
+
const defaultHint = defaultValue ? ` ${dim(`(default: ${defaultValue})`)}` : '';
|
|
406
|
+
|
|
407
|
+
// Handle readline errors
|
|
408
|
+
rl.on('error', () => {
|
|
409
|
+
rl.close();
|
|
410
|
+
process.exit(1);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return new Promise((resolve) => {
|
|
414
|
+
rl.question(`${bold(question)}${defaultHint}: `, (answer) => {
|
|
415
|
+
rl.close();
|
|
416
|
+
const value = answer.trim() || defaultValue;
|
|
417
|
+
console.log(success(` ✓ ${value}\n`));
|
|
418
|
+
resolve(value);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Prompt for runtime selection
|
|
425
|
+
* @param {Object} manifest - Template manifest
|
|
426
|
+
* @returns {Promise<string>} Selected runtime (normalized)
|
|
427
|
+
*/
|
|
428
|
+
export async function promptRuntime(manifest) {
|
|
429
|
+
// Get unique languages from manifest (manifest uses 'language' field)
|
|
430
|
+
const languages = new Set();
|
|
431
|
+
for (const template of manifest.templates) {
|
|
432
|
+
if (template.language) languages.add(template.language);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Build options with display names and template counts
|
|
436
|
+
const languageCounts = {};
|
|
437
|
+
for (const template of manifest.templates) {
|
|
438
|
+
if (!template.language) continue;
|
|
439
|
+
languageCounts[template.language] = (languageCounts[template.language] || 0) + 1;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Map manifest language names to internal runtime identifiers
|
|
443
|
+
// Manifest uses: CSharp, Java, JavaScript, PowerShell, Python, TypeScript, ARM, Bicep, Terraform
|
|
444
|
+
const languageToRuntime = {
|
|
445
|
+
'Python': 'python',
|
|
446
|
+
'JavaScript': 'node',
|
|
447
|
+
'TypeScript': 'node', // Node.js covers both JS/TS
|
|
448
|
+
'CSharp': 'dotnet-isolated',
|
|
449
|
+
'Java': 'java',
|
|
450
|
+
'PowerShell': 'powershell',
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const runtimeDisplayMap = {
|
|
454
|
+
'python': 'Python',
|
|
455
|
+
'node': 'Node.js (TypeScript/JavaScript)',
|
|
456
|
+
'dotnet-isolated': '.NET Isolated (C#)',
|
|
457
|
+
'java': 'Java',
|
|
458
|
+
'powershell': 'PowerShell',
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Filter to supported runtimes and aggregate counts
|
|
462
|
+
const runtimeCounts = {};
|
|
463
|
+
for (const [lang, count] of Object.entries(languageCounts)) {
|
|
464
|
+
const runtime = languageToRuntime[lang];
|
|
465
|
+
if (runtime) {
|
|
466
|
+
runtimeCounts[runtime] = (runtimeCounts[runtime] || 0) + count;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Prioritize common runtimes
|
|
471
|
+
const priorityOrder = ['python', 'node', 'dotnet-isolated', 'java', 'powershell'];
|
|
472
|
+
const sortedRuntimes = Object.keys(runtimeCounts).sort((a, b) => {
|
|
473
|
+
const aIdx = priorityOrder.indexOf(a);
|
|
474
|
+
const bIdx = priorityOrder.indexOf(b);
|
|
475
|
+
if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
|
|
476
|
+
if (aIdx === -1) return 1;
|
|
477
|
+
if (bIdx === -1) return -1;
|
|
478
|
+
return aIdx - bIdx;
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const options = sortedRuntimes.map(rt => ({
|
|
482
|
+
value: rt,
|
|
483
|
+
label: runtimeDisplayMap[rt] || rt,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
return selectPrompt('Select a runtime:', options);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Prompt for Node.js language variant (TypeScript or JavaScript)
|
|
491
|
+
* @returns {Promise<string>} 'typescript' or 'javascript'
|
|
492
|
+
*/
|
|
493
|
+
export async function promptNodeLanguage() {
|
|
494
|
+
const options = [
|
|
495
|
+
{ value: 'typescript', label: `TypeScript ${dim('(recommended)')}` },
|
|
496
|
+
{ value: 'javascript', label: 'JavaScript' },
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
return selectPrompt('Select Node.js language:', options);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Prompt for template selection (triggers, input bindings, output bindings)
|
|
504
|
+
* Shows top 9 templates with "More..." option for additional templates.
|
|
505
|
+
* Supports type-to-filter search (3+ characters).
|
|
506
|
+
* @param {Array} templates - Filtered templates for the selected runtime
|
|
507
|
+
* @param {string[]} priorityOrder - Resource types in priority order
|
|
508
|
+
* @returns {Promise<Object>} Selected template
|
|
509
|
+
*/
|
|
510
|
+
export async function promptTrigger(templates, priorityOrder) {
|
|
511
|
+
const MAX_INITIAL_DISPLAY = 9;
|
|
512
|
+
|
|
513
|
+
// Sort all templates by resource priority
|
|
514
|
+
const sorted = sortTemplatesByResourcePriority(templates, priorityOrder);
|
|
515
|
+
|
|
516
|
+
// Take top 9 for initial display
|
|
517
|
+
const displayTemplates = sorted.slice(0, MAX_INITIAL_DISPLAY);
|
|
518
|
+
const hasMore = sorted.length > MAX_INITIAL_DISPLAY;
|
|
519
|
+
|
|
520
|
+
// Build options list with searchText for filtering
|
|
521
|
+
const options = displayTemplates.map(template => ({
|
|
522
|
+
value: template,
|
|
523
|
+
label: formatTemplateLabel(template),
|
|
524
|
+
searchText: `${template.displayName || ''} ${template.id || ''} ${template.resource || ''} ${template.bindingType || ''}`,
|
|
525
|
+
}));
|
|
526
|
+
|
|
527
|
+
// Add separator and "More..." option if there are additional templates
|
|
528
|
+
if (hasMore) {
|
|
529
|
+
options.push({
|
|
530
|
+
value: '__SEPARATOR__',
|
|
531
|
+
label: dim('────────────────────────────'),
|
|
532
|
+
disabled: true,
|
|
533
|
+
});
|
|
534
|
+
options.push({
|
|
535
|
+
value: '__MORE__',
|
|
536
|
+
label: `${bold('More templates...')} ${dim(`(${sorted.length - MAX_INITIAL_DISPLAY} more)`)}`,
|
|
537
|
+
searchText: 'more show all templates',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (options.length === 0) {
|
|
542
|
+
console.log(dim(' No templates found for this runtime.\n'));
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Use search-enabled prompt from the start
|
|
547
|
+
const selected = await selectPromptWithSearch('Select a template:', options);
|
|
548
|
+
|
|
549
|
+
// If "More..." selected, show full list
|
|
550
|
+
if (selected === '__MORE__') {
|
|
551
|
+
return promptTriggerAll(sorted, priorityOrder);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return selected;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Sort templates by resource type priority, then by template priority (P0 starters first),
|
|
559
|
+
* then by binding type within each resource.
|
|
560
|
+
*
|
|
561
|
+
* Sort order:
|
|
562
|
+
* 1. Resource type (http > blob > timer > queue > servicebus > eventhub > durable > eventgrid > other)
|
|
563
|
+
* 2. Template priority (P0 starters > P1 > P2 samples)
|
|
564
|
+
* 3. Binding type (trigger > input > output > other)
|
|
565
|
+
* 4. Alphabetical by display name
|
|
566
|
+
*/
|
|
567
|
+
function sortTemplatesByResourcePriority(templates, priorityOrder) {
|
|
568
|
+
const bindingTypeOrder = { 'trigger': 0, 'input': 1, 'output': 2 };
|
|
569
|
+
|
|
570
|
+
return [...templates].sort((a, b) => {
|
|
571
|
+
// 1. Sort by resource type priority
|
|
572
|
+
const aResource = (a.resource || 'other').toLowerCase();
|
|
573
|
+
const bResource = (b.resource || 'other').toLowerCase();
|
|
574
|
+
|
|
575
|
+
const aIdx = priorityOrder.indexOf(aResource);
|
|
576
|
+
const bIdx = priorityOrder.indexOf(bResource);
|
|
577
|
+
|
|
578
|
+
// Prioritized resources come first, others go to end alphabetically
|
|
579
|
+
const aResourcePriority = aIdx === -1 ? 999 : aIdx;
|
|
580
|
+
const bResourcePriority = bIdx === -1 ? 999 : bIdx;
|
|
581
|
+
|
|
582
|
+
if (aResourcePriority !== bResourcePriority) {
|
|
583
|
+
return aResourcePriority - bResourcePriority;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// If both unprioritized, sort alphabetically by resource
|
|
587
|
+
if (aResourcePriority === 999 && bResourcePriority === 999) {
|
|
588
|
+
const resourceCmp = aResource.localeCompare(bResource);
|
|
589
|
+
if (resourceCmp !== 0) return resourceCmp;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 2. Within same resource: sort by template priority (P0 starters first, then P1, P2 samples)
|
|
593
|
+
const aTemplatePriority = a.priority ?? 999;
|
|
594
|
+
const bTemplatePriority = b.priority ?? 999;
|
|
595
|
+
|
|
596
|
+
if (aTemplatePriority !== bTemplatePriority) {
|
|
597
|
+
return aTemplatePriority - bTemplatePriority;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 3. Within same priority: sort by binding type (trigger > input > output)
|
|
601
|
+
const aBinding = bindingTypeOrder[a.bindingType] ?? 3;
|
|
602
|
+
const bBinding = bindingTypeOrder[b.bindingType] ?? 3;
|
|
603
|
+
|
|
604
|
+
if (aBinding !== bBinding) {
|
|
605
|
+
return aBinding - bBinding;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 4. Alphabetical within same resource + priority + binding type
|
|
609
|
+
return (a.displayName || a.id).localeCompare(b.displayName || b.id);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Format template label for display
|
|
615
|
+
*/
|
|
616
|
+
function formatTemplateLabel(template) {
|
|
617
|
+
const name = template.displayName || template.id;
|
|
618
|
+
const resource = template.resource || 'other';
|
|
619
|
+
const bindingType = template.bindingType || '';
|
|
620
|
+
|
|
621
|
+
// Show binding type for non-triggers
|
|
622
|
+
if (bindingType && bindingType !== 'trigger') {
|
|
623
|
+
return `${funcName(name)} ${dim(`(${resource} ${bindingType})`)}`;
|
|
624
|
+
}
|
|
625
|
+
return `${funcName(name)} ${dim(`(${resource})`)}`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Show all templates when "More..." is selected
|
|
630
|
+
* Uses search-enabled prompt for easy filtering
|
|
631
|
+
* @param {Array} templates - All templates
|
|
632
|
+
* @param {string[]} priorityOrder - Resource priority order
|
|
633
|
+
* @returns {Promise<object>} Selected template
|
|
634
|
+
*/
|
|
635
|
+
async function promptTriggerAll(templates, priorityOrder) {
|
|
636
|
+
// Sort by resource priority, then binding type, then alphabetically
|
|
637
|
+
const sorted = sortTemplatesByResourcePriority(templates, priorityOrder);
|
|
638
|
+
|
|
639
|
+
const options = sorted.map(template => ({
|
|
640
|
+
value: template,
|
|
641
|
+
label: formatTemplateLabel(template),
|
|
642
|
+
// Add searchText for improved search matching
|
|
643
|
+
searchText: `${template.displayName || ''} ${template.id || ''} ${template.resource || ''} ${template.bindingType || ''}`,
|
|
644
|
+
}));
|
|
645
|
+
|
|
646
|
+
console.log(dim(`\n Showing all ${options.length} templates (type to filter):\n`));
|
|
647
|
+
return selectPromptWithSearch('Select a template:', options);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Prompt for project name
|
|
652
|
+
* @param {string} targetDir - Target directory path
|
|
653
|
+
* @returns {Promise<string>} Project name
|
|
654
|
+
*/
|
|
655
|
+
export async function promptProjectName(targetDir) {
|
|
656
|
+
const { basename } = await import('node:path');
|
|
657
|
+
const defaultName = basename(targetDir) || 'my-function-app';
|
|
658
|
+
return textPrompt('Project name', defaultName);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Prompt for SKU selection
|
|
663
|
+
* @returns {Promise<string>} Selected SKU
|
|
664
|
+
*/
|
|
665
|
+
export async function promptSku() {
|
|
666
|
+
const options = [
|
|
667
|
+
{ value: 'flex', label: `Flex Consumption ${dim('(recommended, serverless)')}` },
|
|
668
|
+
{ value: 'premium', label: `Premium ${dim('(always-warm, VNet integration)')}` },
|
|
669
|
+
{ value: 'dedicated', label: `Dedicated ${dim('(App Service Plan)')}` },
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
return selectPrompt('Select target SKU:', options);
|
|
673
|
+
}
|