@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.
@@ -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
+ }