Skip to content

Commit 4372711

Browse files
authored
Merge pull request #49 from constructive-io/devin/1766834244-refactor-prompts-ui-engine
feat(inquirerer): add engine-based prompt implementations
2 parents 90b2c1b + 8c19cf0 commit 4372711

9 files changed

Lines changed: 916 additions & 21 deletions

File tree

packages/genomic/__tests__/create-gen.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ jest.mock('inquirerer', () => {
2020
};
2121
}),
2222
registerDefaultResolver: jest.fn(),
23+
createSpinner: jest.fn().mockImplementation(() => {
24+
return {
25+
start: jest.fn(),
26+
succeed: jest.fn(),
27+
fail: jest.fn(),
28+
text: jest.fn(),
29+
};
30+
}),
2331
};
2432
});
2533

packages/genomic/src/git/git-cloner.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
22
import * as fs from 'fs';
33
import * as os from 'os';
44
import * as path from 'path';
5+
import { createSpinner } from 'inquirerer';
56
import { GitCloneOptions, GitCloneResult } from './types';
67

78
export class GitCloner {
@@ -97,16 +98,34 @@ export class GitCloner {
9798
const branch = options?.branch;
9899
const depth = options?.depth ?? 1;
99100
const singleBranch = options?.singleBranch ?? true;
101+
const silent = options?.silent ?? true;
100102

101103
const branchArgs = branch ? ` --branch ${branch}` : '';
102104
const singleBranchArgs = singleBranch ? ' --single-branch' : '';
103105
const depthArgs = ` --depth ${depth}`;
104106

105107
const command = `git clone${branchArgs}${singleBranchArgs}${depthArgs} ${url} ${destination}`;
106108

109+
const spinner = silent ? createSpinner(`Cloning ${url}...`) : null;
110+
107111
try {
108-
execSync(command, { stdio: 'inherit' });
112+
if (spinner) {
113+
spinner.start();
114+
}
115+
116+
execSync(command, {
117+
stdio: silent ? 'pipe' : 'inherit',
118+
encoding: 'utf-8'
119+
});
120+
121+
if (spinner) {
122+
spinner.succeed('Repository cloned');
123+
}
109124
} catch (error) {
125+
if (spinner) {
126+
spinner.fail('Failed to clone repository');
127+
}
128+
110129
// Clean up on failure
111130
if (fs.existsSync(destination)) {
112131
fs.rmSync(destination, { recursive: true, force: true });

packages/genomic/src/git/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export interface GitCloneOptions {
22
branch?: string;
33
depth?: number;
44
singleBranch?: boolean;
5+
/** If true (default), show spinner and silence git output. If false, show raw git output. */
6+
silent?: boolean;
57
}
68

79
export interface GitCloneResult {

packages/inquirerer/README.md

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ npm install inquirerer
6262
- [Custom Resolvers](#custom-resolvers)
6363
- [Resolver Examples](#resolver-examples)
6464
- [CLI Helper](#cli-helper)
65+
- [UI Components](#ui-components)
66+
- [Spinner](#spinner)
67+
- [Progress Bar](#progress-bar)
68+
- [Streaming Text](#streaming-text)
69+
- [Custom UI with UIEngine](#custom-ui-with-uiengine)
6570
- [Developing](#developing)
6671

6772
## Quick Start
@@ -1241,4 +1246,204 @@ const handler: CommandHandler = async (argv, prompter) => {
12411246

12421247
const cli = new CLI(handler, options);
12431248
await cli.run();
1244-
```
1249+
```
1250+
1251+
## UI Components
1252+
1253+
inquirerer includes a set of UI components for building rich terminal interfaces beyond simple prompts. These are useful for showing progress, loading states, and streaming output.
1254+
1255+
### Spinner
1256+
1257+
Show an animated spinner while performing async operations:
1258+
1259+
```typescript
1260+
import { createSpinner } from 'inquirerer';
1261+
1262+
const spinner = createSpinner('Loading packages...');
1263+
spinner.start();
1264+
1265+
const data = await fetchPackages();
1266+
1267+
spinner.succeed('Loaded 42 packages');
1268+
// Or: spinner.fail('Failed to load'), spinner.warn('Warning'), spinner.info('Info')
1269+
```
1270+
1271+
**Spinner styles:**
1272+
1273+
```typescript
1274+
import { createSpinner, SPINNER_STYLES } from 'inquirerer';
1275+
1276+
// Use different spinner styles
1277+
const spinner = createSpinner('Processing...', {
1278+
frames: SPINNER_STYLES.dots, // ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
1279+
// frames: SPINNER_STYLES.line, // - \ | /
1280+
// frames: SPINNER_STYLES.arc, // ◜ ◠ ◝ ◞ ◡ ◟
1281+
// frames: SPINNER_STYLES.circle // ◐ ◓ ◑ ◒
1282+
});
1283+
```
1284+
1285+
**Update text while spinning:**
1286+
1287+
```typescript
1288+
spinner.start();
1289+
spinner.text('Step 1: Downloading...');
1290+
await download();
1291+
spinner.text('Step 2: Installing...');
1292+
await install();
1293+
spinner.succeed('Done!');
1294+
```
1295+
1296+
### Progress Bar
1297+
1298+
Show progress for operations with known completion:
1299+
1300+
```typescript
1301+
import { createProgress } from 'inquirerer';
1302+
1303+
const progress = createProgress('Installing dependencies');
1304+
progress.start();
1305+
1306+
for (let i = 0; i < packages.length; i++) {
1307+
await installPackage(packages[i]);
1308+
progress.update((i + 1) / packages.length);
1309+
}
1310+
1311+
progress.complete('All packages installed');
1312+
// Or: progress.error('Installation failed')
1313+
```
1314+
1315+
**Increment progress:**
1316+
1317+
```typescript
1318+
const progress = createProgress('Processing files');
1319+
progress.start();
1320+
1321+
for (const file of files) {
1322+
await processFile(file);
1323+
progress.increment(1 / files.length);
1324+
}
1325+
1326+
progress.complete();
1327+
```
1328+
1329+
### Streaming Text
1330+
1331+
Display streaming output like AI chat responses:
1332+
1333+
```typescript
1334+
import { createStream } from 'inquirerer';
1335+
1336+
const stream = createStream({ showCursor: true });
1337+
stream.start();
1338+
1339+
for await (const token of llmResponse) {
1340+
stream.append(token);
1341+
}
1342+
1343+
stream.done();
1344+
```
1345+
1346+
**With line prefix:**
1347+
1348+
```typescript
1349+
const stream = createStream({ prefix: '> ' });
1350+
stream.start();
1351+
stream.appendLine('First line of response');
1352+
stream.appendLine('Second line of response');
1353+
stream.done();
1354+
```
1355+
1356+
### Custom UI with UIEngine
1357+
1358+
For fully custom interactive UIs, use the `UIEngine` directly:
1359+
1360+
```typescript
1361+
import { UIEngine, Key } from 'inquirerer';
1362+
1363+
interface MyState {
1364+
items: string[];
1365+
selectedIndex: number;
1366+
}
1367+
1368+
const engine = new UIEngine();
1369+
1370+
const result = await engine.run<MyState, string>({
1371+
initialState: {
1372+
items: ['Option A', 'Option B', 'Option C'],
1373+
selectedIndex: 0
1374+
},
1375+
1376+
render: (state) => [
1377+
'Select an option:',
1378+
...state.items.map((item, i) =>
1379+
i === state.selectedIndex ? `> ${item}` : ` ${item}`
1380+
)
1381+
],
1382+
1383+
onEvent: (event, state) => {
1384+
if (event.type === 'key') {
1385+
switch (event.key) {
1386+
case Key.UP:
1387+
return {
1388+
state: {
1389+
...state,
1390+
selectedIndex: Math.max(0, state.selectedIndex - 1)
1391+
}
1392+
};
1393+
case Key.DOWN:
1394+
return {
1395+
state: {
1396+
...state,
1397+
selectedIndex: Math.min(state.items.length - 1, state.selectedIndex + 1)
1398+
}
1399+
};
1400+
case Key.ENTER:
1401+
return {
1402+
state,
1403+
done: true,
1404+
value: state.items[state.selectedIndex]
1405+
};
1406+
}
1407+
}
1408+
return { state };
1409+
}
1410+
});
1411+
1412+
console.log('You selected:', result);
1413+
```
1414+
1415+
**With animations (tick events):**
1416+
1417+
```typescript
1418+
const engine = new UIEngine();
1419+
1420+
await engine.run({
1421+
initialState: { frame: 0, message: 'Loading' },
1422+
tickInterval: 100, // Trigger tick event every 100ms
1423+
1424+
render: (state) => {
1425+
const frames = ['', '', '', '', '', '', '', '', '', ''];
1426+
return [`${frames[state.frame % frames.length]} ${state.message}...`];
1427+
},
1428+
1429+
onEvent: (event, state) => {
1430+
if (event.type === 'tick') {
1431+
return { state: { ...state, frame: state.frame + 1 } };
1432+
}
1433+
if (event.type === 'key' && event.key === Key.ENTER) {
1434+
return { state, done: true };
1435+
}
1436+
return { state };
1437+
}
1438+
});
1439+
```
1440+
1441+
**Run the demos:**
1442+
1443+
```bash
1444+
cd packages/inquirerer
1445+
pnpm dev:spinner # Spinner styles demo
1446+
pnpm dev:chat # Streaming text demo
1447+
pnpm dev:upgrade # Interactive upgrade UI demo
1448+
pnpm dev:prompts # All prompt types demo
1449+
```

0 commit comments

Comments
 (0)