Skip to content

Commit a298407

Browse files
committed
feat(inquirerer): add event-driven UI engine for custom prompts
- Add UIEngine class for building custom interactive terminal UIs - Add Spinner component with multiple styles (dots, line, arc, etc.) - Add ProgressBar component for operations with known completion - Add StreamingText component for AI-style streaming output - Add interactiveUpgrade component for pnpm-style dependency upgrades - Add demo scripts: dev:spinner, dev:chat, dev:upgrade - Export all UI components from main index This enables developers to build rich terminal interfaces with: - Custom rendering via render(state) function - Event-driven updates (key events, timers, async data) - Animated spinners and progress bars - Streaming text display with cursor animation - Interactive multi-select with filtering
1 parent 8f2e433 commit a298407

12 files changed

Lines changed: 1602 additions & 10 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Demo: Chat AI Prompt Box with Streaming
4+
*
5+
* Run with: pnpm dev:chat
6+
* Or: npx ts-node dev/demo-chat.ts
7+
*/
8+
9+
import { createStream, createSpinner } from '../src/ui';
10+
import { cyan, dim, green, white } from 'yanse';
11+
12+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
13+
14+
// Simulated AI responses
15+
const AI_RESPONSES = [
16+
"Hello! I'm an AI assistant. I can help you with coding questions, explain concepts, or assist with various tasks. What would you like to know?",
17+
"TypeScript is a strongly typed programming language that builds on JavaScript. It adds optional static typing and class-based object-oriented programming to the language. Here are some key benefits:\n\n1. **Type Safety**: Catch errors at compile time rather than runtime\n2. **Better IDE Support**: Enhanced autocomplete and refactoring\n3. **Improved Readability**: Types serve as documentation\n4. **Modern Features**: Access to latest ECMAScript features",
18+
"Here's a simple example of a TypeScript function:\n\n```typescript\nfunction greet(name: string): string {\n return `Hello, ${name}!`;\n}\n\nconst message = greet('World');\nconsole.log(message); // Output: Hello, World!\n```\n\nThe `: string` after the parameter and function declaration specifies the types.",
19+
];
20+
21+
/**
22+
* Simulate streaming text character by character
23+
*/
24+
async function streamText(stream: ReturnType<typeof createStream>, text: string) {
25+
const words = text.split(' ');
26+
27+
for (let i = 0; i < words.length; i++) {
28+
const word = words[i];
29+
30+
// Add word character by character for realistic effect
31+
for (const char of word) {
32+
stream.append(char);
33+
await sleep(15 + Math.random() * 25); // Variable typing speed
34+
}
35+
36+
// Add space after word (except last)
37+
if (i < words.length - 1) {
38+
stream.append(' ');
39+
await sleep(10);
40+
}
41+
}
42+
}
43+
44+
async function main() {
45+
console.log('\n' + white('═'.repeat(60)));
46+
console.log(white(' 🤖 AI Chat Demo - Streaming Response Simulation'));
47+
console.log(white('═'.repeat(60)) + '\n');
48+
49+
for (let i = 0; i < AI_RESPONSES.length; i++) {
50+
const response = AI_RESPONSES[i];
51+
52+
// Show user prompt
53+
console.log(cyan('You: ') + dim(`[Question ${i + 1}]`));
54+
console.log('');
55+
56+
// Show thinking spinner
57+
const thinking = createSpinner('Thinking...', { interval: 80 });
58+
thinking.start();
59+
await sleep(800 + Math.random() * 500);
60+
thinking.stop('info', 'Generating response...');
61+
62+
// Stream the response
63+
console.log('');
64+
const stream = createStream({ prefix: green('AI: ') });
65+
stream.start();
66+
67+
await streamText(stream, response);
68+
69+
stream.done();
70+
console.log('\n' + dim('─'.repeat(60)) + '\n');
71+
72+
await sleep(500);
73+
}
74+
75+
console.log(white('═'.repeat(60)));
76+
console.log(white(' ✨ Demo complete!'));
77+
console.log(white('═'.repeat(60)) + '\n');
78+
}
79+
80+
main().catch(console.error);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Demo: Spinners and Loaders
4+
*
5+
* Run with: pnpm dev:spinner
6+
* Or: npx ts-node dev/demo-spinner.ts
7+
*/
8+
9+
import { createSpinner, SPINNER_STYLES } from '../src/ui';
10+
11+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
12+
13+
async function main() {
14+
console.log('\n🎨 Spinner Demo\n');
15+
console.log('This demo shows various spinner styles and states.\n');
16+
17+
// Basic spinner
18+
const spinner1 = createSpinner('Loading packages...');
19+
spinner1.start();
20+
await sleep(2000);
21+
spinner1.succeed('Packages loaded successfully');
22+
23+
await sleep(500);
24+
25+
// Spinner with text updates
26+
const spinner2 = createSpinner('Connecting to server...');
27+
spinner2.start();
28+
await sleep(1000);
29+
spinner2.text('Authenticating...');
30+
await sleep(1000);
31+
spinner2.text('Fetching data...');
32+
await sleep(1000);
33+
spinner2.succeed('Data fetched');
34+
35+
await sleep(500);
36+
37+
// Error state
38+
const spinner3 = createSpinner('Installing dependencies...');
39+
spinner3.start();
40+
await sleep(1500);
41+
spinner3.fail('Failed to install: network error');
42+
43+
await sleep(500);
44+
45+
// Warning state
46+
const spinner4 = createSpinner('Checking for updates...');
47+
spinner4.start();
48+
await sleep(1500);
49+
spinner4.warn('Updates available but not critical');
50+
51+
await sleep(500);
52+
53+
// Info state
54+
const spinner5 = createSpinner('Scanning project...');
55+
spinner5.start();
56+
await sleep(1500);
57+
spinner5.info('Found 42 files');
58+
59+
await sleep(500);
60+
61+
// Different spinner styles
62+
console.log('\n📊 Spinner Styles:\n');
63+
64+
const styles: Array<[string, string[]]> = [
65+
['dots', SPINNER_STYLES.dots],
66+
['line', SPINNER_STYLES.line],
67+
['arc', SPINNER_STYLES.arc],
68+
['circle', SPINNER_STYLES.circle],
69+
['bounce', SPINNER_STYLES.bounce],
70+
['arrow', SPINNER_STYLES.arrow],
71+
['dots2', SPINNER_STYLES.dots2],
72+
];
73+
74+
for (const [name, frames] of styles) {
75+
const spinner = createSpinner(`Style: ${name}`, { frames, interval: 100 });
76+
spinner.start();
77+
await sleep(1500);
78+
spinner.succeed(`${name} complete`);
79+
await sleep(200);
80+
}
81+
82+
console.log('\n✨ Demo complete!\n');
83+
}
84+
85+
main().catch(console.error);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Demo: Interactive Dependency Upgrade UI
4+
*
5+
* Run with: pnpm dev:upgrade
6+
* Or: npx ts-node dev/demo-upgrade.ts
7+
*/
8+
9+
import { upgradePrompt, PackageInfo, createSpinner } from '../src/ui';
10+
import { cyan, green, yellow, dim, white } from 'yanse';
11+
12+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
13+
14+
// Simulated package data (like pnpm outdated would return)
15+
const MOCK_PACKAGES: PackageInfo[] = [
16+
{ name: 'typescript', current: '5.2.2', latest: '5.7.2', type: 'devDependencies' },
17+
{ name: 'react', current: '18.2.0', latest: '19.0.0', type: 'dependencies' },
18+
{ name: 'react-dom', current: '18.2.0', latest: '19.0.0', type: 'dependencies' },
19+
{ name: '@types/node', current: '20.8.0', latest: '22.10.2', type: 'devDependencies' },
20+
{ name: 'eslint', current: '8.50.0', latest: '9.17.0', type: 'devDependencies' },
21+
{ name: 'prettier', current: '3.0.3', latest: '3.4.2', type: 'devDependencies' },
22+
{ name: 'jest', current: '29.6.4', latest: '29.7.0', type: 'devDependencies' },
23+
{ name: 'lodash', current: '4.17.20', latest: '4.17.21', type: 'dependencies' },
24+
{ name: 'axios', current: '1.5.0', latest: '1.7.9', type: 'dependencies' },
25+
{ name: 'zod', current: '3.22.2', latest: '3.24.1', type: 'dependencies' },
26+
{ name: 'vitest', current: '0.34.4', latest: '2.1.8', type: 'devDependencies' },
27+
{ name: '@tanstack/react-query', current: '4.35.3', latest: '5.62.8', type: 'dependencies' },
28+
{ name: 'tailwindcss', current: '3.3.3', latest: '3.4.17', type: 'devDependencies' },
29+
{ name: 'next', current: '13.5.2', latest: '15.1.3', type: 'dependencies' },
30+
{ name: 'prisma', current: '5.3.1', latest: '6.1.0', type: 'devDependencies' },
31+
];
32+
33+
async function main() {
34+
console.log('\n' + white('═'.repeat(70)));
35+
console.log(white(' 📦 Interactive Dependency Upgrade Demo'));
36+
console.log(white('═'.repeat(70)) + '\n');
37+
38+
// Show loading spinner first
39+
const spinner = createSpinner('Checking for outdated packages...');
40+
spinner.start();
41+
await sleep(1500);
42+
spinner.succeed(`Found ${MOCK_PACKAGES.length} packages with updates available`);
43+
44+
console.log('');
45+
console.log(dim('Controls:'));
46+
console.log(dim(' ↑/↓ Navigate packages'));
47+
console.log(dim(' SPACE Toggle selection'));
48+
console.log(dim(' → Change target version'));
49+
console.log(dim(' ENTER Confirm selection'));
50+
console.log(dim(' ESC Cancel'));
51+
console.log(dim(' Type Filter packages'));
52+
console.log('');
53+
54+
try {
55+
const result = await upgradePrompt(MOCK_PACKAGES, 10);
56+
57+
console.log('');
58+
59+
if (result.updates.length === 0) {
60+
console.log(yellow('No packages selected for upgrade.'));
61+
} else {
62+
console.log(green(`\n✔ Selected ${result.updates.length} packages for upgrade:\n`));
63+
64+
for (const update of result.updates) {
65+
console.log(` ${cyan(update.name.padEnd(30))} ${dim(update.from)} ${dim('→')} ${green(update.to)}`);
66+
}
67+
68+
console.log('');
69+
console.log(dim('In a real scenario, this would run:'));
70+
console.log(dim(` pnpm update ${result.updates.map(u => `${u.name}@${u.to}`).join(' ')}`));
71+
}
72+
} catch (error) {
73+
console.error('Error:', error);
74+
}
75+
76+
console.log('\n' + white('═'.repeat(70)));
77+
console.log(white(' ✨ Demo complete!'));
78+
console.log(white('═'.repeat(70)) + '\n');
79+
}
80+
81+
main().catch(console.error);

packages/inquirerer/package.json

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
"bugs": {
2020
"url": "https://github.com/constructive-io/dev-utils/issues"
2121
},
22-
"scripts": {
23-
"copy": "makage assets",
24-
"clean": "makage clean",
25-
"prepublishOnly": "npm run build",
26-
"build": "makage build",
27-
"dev": "ts-node dev/index",
28-
"test": "jest",
29-
"test:watch": "jest --watch"
30-
},
22+
"scripts": {
23+
"copy": "makage assets",
24+
"clean": "makage clean",
25+
"prepublishOnly": "npm run build",
26+
"build": "makage build",
27+
"dev": "ts-node dev/index",
28+
"dev:spinner": "ts-node dev/demo-spinner",
29+
"dev:chat": "ts-node dev/demo-chat",
30+
"dev:upgrade": "ts-node dev/demo-upgrade",
31+
"test": "jest",
32+
"test:watch": "jest --watch"
33+
},
3134
"dependencies": {
3235
"deepmerge": "^4.3.1",
3336
"find-and-require-package-json": "workspace:*",

packages/inquirerer/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './commander';
22
export * from './prompt';
33
export * from './question';
4-
export * from './resolvers';
4+
export * from './resolvers';
5+
export * from './ui';

0 commit comments

Comments
 (0)