chore: added ability to select typescript / framework options 81cb55d6
Steve · 2025-10-18 16:58 1 file(s) · +114 −48
src/index.ts +114 −48
76 76
	}
77 77
}
78 78
79 +
interface MenuItem {
80 +
	value: string;
81 +
	label: string;
82 +
}
83 +
84 +
async function selectFromMenu(
85 +
	question: string,
86 +
	items: MenuItem[],
87 +
	defaultIndex = 0,
88 +
): Promise<string> {
89 +
	return new Promise((resolve) => {
90 +
		let selectedIndex = defaultIndex;
91 +
		let isFirstRender = true;
92 +
93 +
		const renderMenu = () => {
94 +
			// Clear previous menu (move cursor up and clear lines)
95 +
			if (!isFirstRender) {
96 +
				// Move cursor up by the number of lines we printed
97 +
				// +2 for the blank line before question and the question itself
98 +
				process.stdout.write(`\x1b[${items.length + 2}A`);
99 +
				// Clear from cursor to end of screen
100 +
				process.stdout.write("\x1b[0J");
101 +
			}
102 +
			isFirstRender = false;
103 +
104 +
			// Use process.stdout.write for precise control
105 +
			process.stdout.write(colors.blue(`\n${question}\n`));
106 +
			items.forEach((item, index) => {
107 +
				const prefix = index === selectedIndex ? colors.green("❯ ") : "  ";
108 +
				const text =
109 +
					index === selectedIndex ? colors.cyan(item.label) : item.label;
110 +
				process.stdout.write(prefix + text + "\n");
111 +
			});
112 +
		};
113 +
114 +
		const onKeyPress = (str: string, key: any) => {
115 +
			if (key.name === "return" || key.name === "enter") {
116 +
				cleanup();
117 +
				resolve(items[selectedIndex].value);
118 +
				return;
119 +
			}
120 +
121 +
			if (key.name === "up" || str === "k") {
122 +
				selectedIndex =
123 +
					selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
124 +
				renderMenu();
125 +
			} else if (key.name === "down" || str === "j") {
126 +
				selectedIndex =
127 +
					selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
128 +
				renderMenu();
129 +
			} else if (key.ctrl && key.name === "c") {
130 +
				cleanup();
131 +
				process.exit(0);
132 +
			}
133 +
		};
134 +
135 +
		const cleanup = () => {
136 +
			if (process.stdin.isTTY && process.stdin.setRawMode) {
137 +
				process.stdin.setRawMode(false);
138 +
			}
139 +
			process.stdin.removeListener("keypress", onKeyPress);
140 +
			process.stdin.pause();
141 +
			console.log(); // Add newline after selection
142 +
		};
143 +
144 +
		// Enable keypress events
145 +
		const readline = require("readline");
146 +
		readline.emitKeypressEvents(process.stdin);
147 +
148 +
		if (process.stdin.isTTY && process.stdin.setRawMode) {
149 +
			process.stdin.setRawMode(true);
150 +
		}
151 +
152 +
		process.stdin.resume(); // Ensure stdin is resumed
153 +
		process.stdin.on("keypress", onKeyPress);
154 +
		renderMenu();
155 +
	});
156 +
}
157 +
79 158
async function init() {
80 159
	console.log(colors.yellow("∅ Initializing norns project..."));
81 160
112 191
	// Get framework selection
113 192
	let framework: "typescript" | "react" | "svelte" | "vue" = "typescript";
114 193
	if (includeTypes) {
115 -
		console.log(colors.blue("\n▸ Select your framework:"));
116 -
		console.log(colors.cyan("  1. TypeScript (standard)"));
117 -
		console.log(colors.cyan("  2. React"));
118 -
		console.log(colors.cyan("  3. Svelte"));
119 -
		console.log(colors.cyan("  4. Vue"));
120 -
121 -
		const frameworkChoice = await promptUser("Enter your choice (1-4)", "1");
122 -
123 -
		switch (frameworkChoice) {
124 -
			case "1":
125 -
				framework = "typescript";
126 -
				break;
127 -
			case "2":
128 -
				framework = "react";
129 -
				break;
130 -
			case "3":
131 -
				framework = "svelte";
132 -
				break;
133 -
			case "4":
134 -
				framework = "vue";
135 -
				break;
136 -
			default:
137 -
				console.log(
138 -
					colors.yellow(`▸ Invalid choice, defaulting to TypeScript`),
139 -
				);
140 -
				framework = "typescript";
141 -
		}
194 +
		const frameworkChoice = await selectFromMenu(
195 +
			"Select your framework (use arrow keys or j/k, press Enter to select):",
196 +
			[
197 +
				{ value: "typescript", label: "TypeScript (standard)" },
198 +
				{ value: "react", label: "React" },
199 +
				{ value: "svelte", label: "Svelte" },
200 +
				{ value: "vue", label: "Vue" },
201 +
			],
202 +
			0,
203 +
		);
204 +
		framework = frameworkChoice as "typescript" | "react" | "svelte" | "vue";
142 205
	}
143 206
144 207
	// Create the configuration
393 456
const command = positionals[0];
394 457
const componentName = positionals[1];
395 458
396 -
if (values.help) {
397 -
	showHelp();
398 -
} else {
399 -
	switch (command) {
400 -
		case "init":
401 -
			await init();
402 -
			break;
403 -
		case "add":
404 -
			await addComponent(componentName);
405 -
			break;
406 -
		case "help":
407 -
			showHelp();
408 -
			break;
409 -
		default:
410 -
			if (!command) {
459 +
// Wrap in async IIFE to avoid top-level await warning
460 +
(async () => {
461 +
	if (values.help) {
462 +
		showHelp();
463 +
	} else {
464 +
		switch (command) {
465 +
			case "init":
466 +
				await init();
467 +
				break;
468 +
			case "add":
469 +
				await addComponent(componentName);
470 +
				break;
471 +
			case "help":
411 472
				showHelp();
412 -
			} else {
413 -
				console.error(colors.red(`✗ Unknown command: ${command}`));
414 -
				showHelp();
415 -
				process.exit(1);
416 -
			}
473 +
				break;
474 +
			default:
475 +
				if (!command) {
476 +
					showHelp();
477 +
				} else {
478 +
					console.error(colors.red(`✗ Unknown command: ${command}`));
479 +
					showHelp();
480 +
					process.exit(1);
481 +
				}
482 +
		}
417 483
	}
418 -
}
484 +
})();