Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@inquirer/core
Advanced tools
@inquirer/core
The @inquirer/core
package is the library enabling the creation of Inquirer prompts.
It aims to implements a lightweight API similar to React hooks - but without JSX.
npm | yarn |
---|---|
|
|
Visual terminal apps are at their core strings rendered onto the terminal.
The most basic prompt is a function returning a string that'll be rendered in the terminal. This function will run every time the prompt state change, and the new returned string will replace the previously rendered one. The prompt cursor appears after the string.
Wrapping the rendering function with createPrompt()
will setup the rendering layer, inject the state management utilities, and wait until the done
callback is called.
import { createPrompt } from '@inquirer/core';
const input = createPrompt((config, done) => {
// Implement logic
return '? My question';
});
// And it is then called as
const answer = await input({
/* config */
});
State management and user interactions are handled through hooks. Hooks are common within the React ecosystem, and Inquirer reimplement the common ones.
State lets a component “remember” information like user input. For example, an input prompt can use state to store the input value, while a list prompt can use state to track the cursor index.
useState
declares a state variable that you can update directly.
import { createPrompt, useState } from '@inquirer/core';
const input = createPrompt((config, done) => {
const [index, setIndex] = useState(0);
// ...
Almost all prompts need to react to user actions. In a terminal, this is done through typing.
useKeypress
allows you to react to keypress events, and access the prompt line.
const input = createPrompt((config, done) => {
useKeypress((key) => {
if (key.name === 'enter') {
done(answer);
}
});
// ...
Behind the scenes, Inquirer prompts are wrappers around readlines. Aside the keypress event object, the hook also pass the active readline instance to the event handler.
const input = createPrompt((config, done) => {
useKeypress((key, readline) => {
setValue(readline.line);
});
// ...
Refs let a prompt hold some information that isn’t used for rendering, like a class instance or a timeout ID. Unlike with state, updating a ref does not re-render your prompt. Refs are an “escape hatch” from the rendering paradigm.
useRef
declares a ref. You can hold any value in it, but most often it’s used to hold a timeout ID.
const input = createPrompt((config, done) => {
const timeout = useRef(null);
// ...
Effects let a prompt connect to and synchronize with external systems. This includes dealing with network or animations.
useEffect
connects a component to an external system.
const chat = createPrompt((config, done) => {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
// ...
A common way to optimize re-rendering performance is to skip unnecessary work. For example, you can tell Inquirer to reuse a cached calculation or to skip a re-render if the data has not changed since the previous render.
useMemo
lets you cache the result of an expensive calculation.
const todoSelect = createPrompt((config, done) => {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
All default prompts, and most custom ones, uses a prefix at the beginning of the prompt line. This helps visually delineate different questions, and provides a convenient area to render a loading spinner.
usePrefix
is a built-in hook to do this.
const input = createPrompt((config, done) => {
const prefix = usePrefix({ status });
return `${prefix} My question`;
});
When looping through a long list of options (like in the select
prompt), paginating the results appearing on the screen at once can be necessary. The usePagination
hook is the utility used within the select
and checkbox
prompts to cycle through the list of options.
Pagination works by taking in the list of options and returning a subset of the rendered items that fit within the page. The hook takes in a few options. It needs a list of options (items
), and a pageSize
which is the number of lines to be rendered. The active
index is the index of the currently selected/selectable item. The loop
option is a boolean that indicates if the list should loop around when reaching the end: this is the default behavior. The pagination hook renders items only as necessary, so it takes a function that can render an item at an index, including an active
state, called renderItem
.
export default createPrompt((config, done) => {
const [active, setActive] = useState(0);
const allChoices = config.choices.map((choice) => choice.name);
const page = usePagination({
items: allChoices,
active: active,
renderItem: ({ item, index, isActive }) => `${isActive ? ">" : " "}${index}. ${item.toString()}`
pageSize: config.pageSize,
loop: config.loop,
});
return `... ${page}`;
});
createPrompt()
APIAs we saw earlier, the rendering function should return a string, and eventually call done
to close the prompt and return the answer.
const input = createPrompt((config, done) => {
const [value, setValue] = useState();
useKeypress((key, readline) => {
if (key.name === 'enter') {
done(answer);
} else {
setValue(readline.line);
}
});
return `? ${config.message} ${value}`;
});
The rendering function can also return a tuple of 2 string ([string, string]
.) The first string represents the prompt. The second one is content to render under the prompt, like an error message. The text input cursor will appear after the first string.
const number = createPrompt((config, done) => {
// Add some logic here
return [`? My question ${input}`, `! The input must be a number`];
});
If using typescript, createPrompt
takes 2 generic arguments.
// createPrompt<Value, Config>
const input = createPrompt<string, { message: string }>(// ...
The first one is the type of the resolved value
const answer: string = await input();
The second one is the type of the prompt config; in other words the interface the created prompt will provide to users.
const answer = await input({
message: 'My question',
});
Listening for keypress events inside an inquirer prompt is a very common pattern. To ease this, we export a few utility functions taking in the keypress event object and return a boolean:
isEnterKey()
isBackspaceKey()
isSpaceKey()
isUpKey()
- Note: this utility will handle vim and emacs keybindings (up, k
, and ctrl+p
)isDownKey()
- Note: this utility will handle vim and emacs keybindings (down, j
, and ctrl+n
)isNumberKey()
one of 1, 2, 3, 4, 5, 6, 7, 8, 9, 0Theming utilities will allow you to expose customization of the prompt style. Inquirer also has a few standard theme values shared across all the official prompts.
To allow standard customization:
import { createPrompt, usePrefix, makeTheme, type Theme } from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
type PromptConfig = {
theme?: PartialDeep<Theme>;
};
export default createPrompt<string, PromptConfig>((config, done) => {
const theme = makeTheme(config.theme);
const prefix = usePrefix({ status, theme });
return `${prefix} ${theme.style.highlight('hello')}`;
});
To setup a custom theme:
import { createPrompt, makeTheme, type Theme } from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';
type PromptTheme = {};
const promptTheme: PromptTheme = {
icon: '!',
};
type PromptConfig = {
theme?: PartialDeep<Theme<PromptTheme>>;
};
export default createPrompt<string, PromptConfig>((config, done) => {
const theme = makeTheme(promptTheme, config.theme);
const prefix = usePrefix({ status, theme });
return `${prefix} ${theme.icon}`;
});
type DefaultTheme = {
prefix: string | { idle: string; done: string };
spinner: {
interval: number;
frames: string[];
};
style: {
answer: (text: string) => string;
message: (text: string, status: 'idle' | 'done' | 'loading') => string;
error: (text: string) => string;
defaultAnswer: (text: string) => string;
help: (text: string) => string;
highlight: (text: string) => string;
key: (text: string) => string;
};
};
You can refer to any @inquirer/prompts
prompts for real examples:
import colors from 'yoctocolors';
import {
createPrompt,
useState,
useKeypress,
isEnterKey,
usePrefix,
type Status,
} from '@inquirer/core';
const confirm = createPrompt<boolean, { message: string; default?: boolean }>(
(config, done) => {
const [status, setStatus] = useState<Status>('idle');
const [value, setValue] = useState('');
const prefix = usePrefix({});
useKeypress((key, rl) => {
if (isEnterKey(key)) {
const answer = value ? /^y(es)?/i.test(value) : config.default !== false;
setValue(answer ? 'yes' : 'no');
setStatus('done');
done(answer);
} else {
setValue(rl.line);
}
});
let formattedValue = value;
let defaultValue = '';
if (status === 'done') {
formattedValue = colors.cyan(value);
} else {
defaultValue = colors.dim(config.default === false ? ' (y/N)' : ' (Y/n)');
}
const message = colors.bold(config.message);
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
},
);
/**
* Which then can be used like this:
*/
const answer = await confirm({ message: 'Do you want to continue?' });
Copyright (c) 2023 Simon Boudrias (twitter: @vaxilart)
Licensed under the MIT license.
FAQs
Core Inquirer prompt API
The npm package @inquirer/core receives a total of 5,783,205 weekly downloads. As such, @inquirer/core popularity was classified as popular.
We found that @inquirer/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.