Socket
Socket
Sign inDemoInstall

prompts

Package Overview
Dependencies
2
Maintainers
1
Versions
36
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.0.4 to 2.1.0

dist/elements/autocompleteMultiselect.js

1

dist/elements/index.js

@@ -11,3 +11,4 @@ 'use strict';

AutocompletePrompt: require('./autocomplete'),
AutocompleteMultiselectPrompt: require('./autocompleteMultiselect'),
ConfirmPrompt: require('./confirm')
};

@@ -5,7 +5,7 @@ 'use strict';

const Prompt = require('./prompt');
const _require = require('sisteransi'),
cursor = _require.cursor;
const Prompt = require('./prompt');
const _require2 = require('../util'),

@@ -34,4 +34,7 @@ clear = _require2.clear,

this.cursor = opts.cursor || 0;
this.hint = opts.hint || '- Space to select. Return to submit';
this.warn = opts.warn || '- This option is disabled';
this.scrollIndex = opts.cursor || 0;
this.hint = opts.hint || '';
this.warn = opts.warn || '- This option is disabled -';
this.minSelected = opts.min;
this.showMinError = false;
this.maxChoices = opts.max;

@@ -51,3 +54,6 @@ this.value = opts.choices.map((ch, idx) => {

this.clear = clear('');
this.render();
if (!opts.overrideRender) {
this.render();
}
}

@@ -75,4 +81,7 @@

submit() {
if (this.value[this.cursor].disabled) {
this.bell();
const selected = this.value.filter(e => e.selected);
if (this.minSelected && selected.length < this.minSelected) {
this.showMinError = true;
this.render();
} else {

@@ -104,4 +113,8 @@ this.done = true;

up() {
if (this.cursor === 0) return this.bell();
this.cursor--;
if (this.cursor === 0) {
this.cursor = this.value.length - 1;
} else {
this.cursor--;
}
this.render();

@@ -111,4 +124,8 @@ }

down() {
if (this.cursor === this.value.length - 1) return this.bell();
this.cursor++;
if (this.cursor === this.value.length - 1) {
this.cursor = 0;
} else {
this.cursor++;
}
this.render();

@@ -128,4 +145,3 @@ }

_(c, key) {
if (c !== ' ') return this.bell();
handleSpaceToggle() {
const v = this.value[this.cursor];

@@ -144,2 +160,79 @@

_(c, key) {
if (c === ' ') {
this.handleSpaceToggle();
} else {
return this.bell();
}
}
renderInstructions() {
return `
Instructions:
${figures.arrowUp}/${figures.arrowDown}: Highlight option
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection
enter/return: Complete answer
`;
}
renderOption(cursor, v, i) {
let title;
if (v.disabled) title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);else title = cursor === i ? color.cyan().underline(v.title) : v.title;
return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + title;
} // shared with autocompleteMultiselect
paginateOptions(options) {
const c = this.cursor;
let styledOptions = options.map((v, i) => this.renderOption(c, v, i));
const numOfOptionsToRender = 10; // if needed, can add an option to change this.
let scopedOptions = styledOptions;
let hint = '';
if (styledOptions.length === 0) {
return color.red('No matches for this query.');
} else if (styledOptions.length > numOfOptionsToRender) {
let startIndex = c - numOfOptionsToRender / 2;
let endIndex = c + numOfOptionsToRender / 2;
if (startIndex < 0) {
startIndex = 0;
endIndex = numOfOptionsToRender;
} else if (endIndex > options.length) {
endIndex = options.length;
startIndex = endIndex - numOfOptionsToRender;
}
scopedOptions = styledOptions.slice(startIndex, endIndex);
hint = color.dim('(Move up and down to reveal more choices)');
}
return '\n' + scopedOptions.join('\n') + '\n' + hint;
} // shared with autocomleteMultiselect
renderOptions(options) {
if (!this.done) {
return this.paginateOptions(options);
}
return '';
}
renderDoneOrInstructions() {
if (this.done) {
const selected = this.value.filter(e => e.selected).map(v => v.title).join(', ');
return selected;
}
const output = [color.gray(this.hint), this.renderInstructions()];
if (this.value[this.cursor].disabled) {
output.push(color.yellow(this.warn));
}
return output.join(' ');
}
render() {

@@ -150,14 +243,10 @@ if (this.closed) return;

const selected = this.value.filter(e => e.selected).map(v => v.title).join(', ');
let prompt = [style.symbol(this.done, this.aborted), color.bold(this.msg), style.delimiter(false), this.done ? selected : this.value[this.cursor].disabled ? color.yellow(this.warn) : color.gray(this.hint)].join(' '); // print choices
let prompt = [style.symbol(this.done, this.aborted), color.bold(this.msg), style.delimiter(false), this.renderDoneOrInstructions()].join(' ');
if (!this.done) {
const c = this.cursor;
prompt += '\n' + this.value.map((v, i) => {
let title;
if (v.disabled) title = c === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);else title = c === i ? color.cyan().underline(v.title) : v.title;
return (v.selected ? color.green(figures.tick) : ' ') + ' ' + title;
}).join('\n');
if (this.showMinError) {
prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`);
this.showMinError = false;
}
prompt += this.renderOptions(this.value);
this.out.write(this.clear + prompt);

@@ -164,0 +253,0 @@ this.clear = clear(prompt);

@@ -163,3 +163,3 @@ 'use strict';

/**
* Interactive multi-select prompt
* Interactive multi-select / autocompleteMultiselect prompt
* @param {string} args.message Prompt message to display

@@ -188,2 +188,13 @@ * @param {Array} args.choices Array of choices objects `[{ title, value, [selected] }, ...]`

$.autocompleteMultiselect = args => {
args.choices = [].concat(args.choices || []);
const toSelected = items => items.filter(item => item.selected).map(item => item.value);
return toPrompt('AutocompleteMultiselectPrompt', args, {
onAbort: toSelected,
onSubmit: toSelected
});
};
const byTitle = (input, choices) => Promise.resolve(choices.filter(item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase()));

@@ -190,0 +201,0 @@ /**

'use strict';
const main = {
arrowUp: '↑',
arrowDown: '↓',
arrowLeft: '←',
arrowRight: '→',
radioOn: '◉',
radioOff: '◯',
tick: '✔',

@@ -12,2 +18,8 @@ cross: '✖',

const win = {
arrowUp: main.arrowUp,
arrowDown: main.arrowDown,
arrowLeft: main.arrowLeft,
arrowRight: main.arrowRight,
radioOn: '(*)',
radioOff: '( )',
tick: '√',

@@ -14,0 +26,0 @@ cross: '×',

@@ -0,4 +1,14 @@

function isNodeLT(tar) {
tar = (Array.isArray(tar) ? tar : tar.split('.')).map(Number);
let i=0, src=process.versions.node.split('.').map(Number);
for (; i < tar.length; i++) {
if (src[i] > tar[i]) return false;
if (tar[i] > src[i]) return true;
}
return false;
}
module.exports =
parseInt(process.versions.node, 10) < 8
isNodeLT('8.6.0')
? require('./dist/index.js')
: require('./lib/index.js');

@@ -11,3 +11,4 @@ 'use strict';

AutocompletePrompt: require('./autocomplete'),
AutocompleteMultiselectPrompt: require('./autocompleteMultiselect'),
ConfirmPrompt: require('./confirm')
};

138

lib/elements/multiselect.js
'use strict';
const color = require('kleur');
const { cursor } = require('sisteransi');
const Prompt = require('./prompt');
const { cursor } = require('sisteransi');
const { clear, figures, style } = require('../util');

@@ -25,4 +25,7 @@

this.cursor = opts.cursor || 0;
this.hint = opts.hint || '- Space to select. Return to submit';
this.warn = opts.warn || '- This option is disabled';
this.scrollIndex = opts.cursor || 0;
this.hint = opts.hint || '';
this.warn = opts.warn || '- This option is disabled -';
this.minSelected = opts.min;
this.showMinError = false;
this.maxChoices = opts.max;

@@ -40,3 +43,5 @@ this.value = opts.choices.map((ch, idx) => {

this.clear = clear('');
this.render();
if (!opts.overrideRender) {
this.render();
}
}

@@ -64,4 +69,7 @@

submit() {
if(this.value[this.cursor].disabled) {
this.bell();
const selected = this.value
.filter(e => e.selected);
if (this.minSelected && selected.length < this.minSelected) {
this.showMinError = true;
this.render();
} else {

@@ -92,4 +100,7 @@ this.done = true;

up() {
if (this.cursor === 0) return this.bell();
this.cursor--;
if (this.cursor === 0) {
this.cursor = this.value.length - 1;
} else {
this.cursor--;
}
this.render();

@@ -99,4 +110,7 @@ }

down() {
if (this.cursor === this.value.length - 1) return this.bell();
this.cursor++;
if (this.cursor === this.value.length - 1) {
this.cursor = 0;
} else {
this.cursor++;
}
this.render();

@@ -116,4 +130,3 @@ }

_(c, key) {
if (c !== ' ') return this.bell();
handleSpaceToggle() {
const v = this.value[this.cursor];

@@ -132,2 +145,77 @@

_(c, key) {
if (c === ' ') {
this.handleSpaceToggle();
} else {
return this.bell();
}
}
renderInstructions() {
return `
Instructions:
${figures.arrowUp}/${figures.arrowDown}: Highlight option
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection
enter/return: Complete answer
`
}
renderOption(cursor, v, i) {
let title;
if (v.disabled) title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
else title = cursor === i ? color.cyan().underline(v.title) : v.title;
return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + title
}
// shared with autocompleteMultiselect
paginateOptions(options) {
const c = this.cursor;
let styledOptions = options.map((v, i) => this.renderOption(c, v, i));
const numOfOptionsToRender = 10; // if needed, can add an option to change this.
let scopedOptions = styledOptions;
let hint = '';
if (styledOptions.length === 0) {
return color.red('No matches for this query.');
} else if (styledOptions.length > numOfOptionsToRender) {
let startIndex = c - (numOfOptionsToRender / 2);
let endIndex = c + (numOfOptionsToRender / 2);
if (startIndex < 0) {
startIndex = 0;
endIndex = numOfOptionsToRender;
} else if (endIndex > options.length) {
endIndex = options.length;
startIndex = endIndex - numOfOptionsToRender;
}
scopedOptions = styledOptions.slice(startIndex, endIndex);
hint = color.dim('(Move up and down to reveal more choices)');
}
return '\n' + scopedOptions.join('\n') + '\n' + hint;
}
// shared with autocomleteMultiselect
renderOptions(options) {
if (!this.done) {
return this.paginateOptions(options);
}
return '';
}
renderDoneOrInstructions() {
if (this.done) {
const selected = this.value
.filter(e => e.selected)
.map(v => v.title)
.join(', ');
return selected;
}
const output = [color.gray(this.hint), this.renderInstructions()];
if (this.value[this.cursor].disabled) {
output.push(color.yellow(this.warn));
}
return output.join(' ');
}
render() {

@@ -139,6 +227,3 @@ if (this.closed) return;

// print prompt
const selected = this.value
.filter(e => e.selected)
.map(v => v.title)
.join(', ');
let prompt = [

@@ -148,20 +233,9 @@ style.symbol(this.done, this.aborted),

style.delimiter(false),
this.done ? selected : this.value[this.cursor].disabled
? color.yellow(this.warn) : color.gray(this.hint)
this.renderDoneOrInstructions()
].join(' ');
// print choices
if (!this.done) {
const c = this.cursor;
prompt +=
'\n' +
this.value
.map((v, i) => {
let title;
if (v.disabled) title = c === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
else title = c === i ? color.cyan().underline(v.title) : v.title;
return (v.selected ? color.green(figures.tick) : ' ') + ' ' + title
})
.join('\n');
if (this.showMinError) {
prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`);
this.showMinError = false;
}
prompt += this.renderOptions(this.value);

@@ -168,0 +242,0 @@ this.out.write(this.clear + prompt);

@@ -152,3 +152,3 @@ 'use strict';

/**
* Interactive multi-select prompt
* Interactive multi-select / autocompleteMultiselect prompt
* @param {string} args.message Prompt message to display

@@ -173,2 +173,11 @@ * @param {Array} args.choices Array of choices objects `[{ title, value, [selected] }, ...]`

$.autocompleteMultiselect = args => {
args.choices = [].concat(args.choices || []);
const toSelected = items => items.filter(item => item.selected).map(item => item.value);
return toPrompt('AutocompleteMultiselectPrompt', args, {
onAbort: toSelected,
onSubmit: toSelected
});
};
const byTitle = (input, choices) => Promise.resolve(

@@ -175,0 +184,0 @@ choices.filter(item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase())

@@ -1,21 +0,33 @@

'use strict';
'use strict';
const main = {
tick: '✔',
cross: '✖',
ellipsis: '…',
pointerSmall: '›',
line: '─',
pointer: '❯'
};
const main = {
arrowUp: '↑',
arrowDown: '↓',
arrowLeft: '←',
arrowRight: '→',
radioOn: '◉',
radioOff: '◯',
tick: '✔',
cross: '✖',
ellipsis: '…',
pointerSmall: '›',
line: '─',
pointer: '❯'
};
const win = {
tick: '√',
cross: '×',
ellipsis: '...',
pointerSmall: '»',
line: '─',
pointer: '>'
};
const figures = process.platform === 'win32' ? win : main;
arrowUp: main.arrowUp,
arrowDown: main.arrowDown,
arrowLeft: main.arrowLeft,
arrowRight: main.arrowRight,
radioOn: '(*)',
radioOff: '( )',
tick: '√',
cross: '×',
ellipsis: '...',
pointerSmall: '»',
line: '─',
pointer: '>'
};
const figures = process.platform === 'win32' ? win : main;
module.exports = figures;
module.exports = figures;
{
"name": "prompts",
"version": "2.0.4",
"version": "2.1.0",
"description": "Lightweight, beautiful and user-friendly prompts",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -59,3 +59,4 @@ <p align="center">

const response = await prompts({
(async () => {
const response = await prompts({
type: 'number',

@@ -65,8 +66,9 @@ name: 'value',

validate: value => value < 18 ? `Nightclub is 18+ only` : true
});
});
console.log(response); // => { value: 24 }
console.log(response); // => { value: 24 }
})();
```
> Examples are meant to be illustrative. `await` calls need to be run within an async function. See [`example.js`](https://github.com/terkelg/prompts/blob/master/example.js).
> See [`example.js`](https://github.com/terkelg/prompts/blob/master/example.js) for more options.

@@ -86,9 +88,11 @@

let response = await prompts({
(async () => {
const response = await prompts({
type: 'text',
name: 'meaning',
message: 'What is the meaning of life?'
});
});
console.log(response.meaning);
console.log(response.meaning);
})();
```

@@ -104,24 +108,26 @@

let questions = [
{
type: 'text',
name: 'username',
message: 'What is your GitHub username?'
},
{
type: 'number',
name: 'age',
message: 'How old are you?'
},
{
type: 'text',
name: 'about',
message: 'Tell something about yourself',
initial: 'Why should I?'
}
const questions = [
{
type: 'text',
name: 'username',
message: 'What is your GitHub username?'
},
{
type: 'number',
name: 'age',
message: 'How old are you?'
},
{
type: 'text',
name: 'about',
message: 'Tell something about yourself',
initial: 'Why should I?'
}
];
let response = await prompts(questions);
(async () => {
const response = await prompts(questions);
// => response => { username, age, about }
// => response => { username, age, about }
})();
```

@@ -137,16 +143,18 @@

let questions = [
{
type: 'text',
name: 'dish',
message: 'Do you like pizza?'
},
{
type: prev => prev == 'pizza' ? 'text' : null,
name: 'topping',
message: 'Name a topping'
}
const questions = [
{
type: 'text',
name: 'dish',
message: 'Do you like pizza?'
},
{
type: prev => prev == 'pizza' ? 'text' : null,
name: 'topping',
message: 'Name a topping'
}
];
let response = await prompts(questions);
(async () => {
const response = await prompts(questions);
})();
```

@@ -189,5 +197,7 @@

```js
let questions = [{ ... }];
let onSubmit = (prompt, response) => console.log(`Thanks I got ${response} from ${prompt.name}`);
let response = await prompts(questions, { onSubmit });
(async () => {
const questions = [{ ... }];
const onSubmit = (prompt, response) => console.log(`Thanks I got ${response} from ${prompt.name}`);
const response = await prompts(questions, { onSubmit });
})();
```

@@ -208,8 +218,10 @@

```js
let questions = [{ ... }];
let onCancel = prompt => {
console.log('Never stop prompting!');
return true;
}
let response = await prompts(questions, { onCancel });
(async () => {
const questions = [{ ... }];
const onCancel = prompt => {
console.log('Never stop prompting!');
return true;
}
const response = await prompts(questions, { onCancel });
})();
```

@@ -229,21 +241,23 @@

const response = await prompts([
{
type: 'text',
name: 'twitter',
message: `What's your twitter handle?`
},
{
type: 'multiselect',
name: 'color',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00' },
{ title: 'Blue', value: '#0000ff' }
],
}
]);
(async () => {
const response = await prompts([
{
type: 'text',
name: 'twitter',
message: `What's your twitter handle?`
},
{
type: 'multiselect',
name: 'color',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00' },
{ title: 'Blue', value: '#0000ff' }
],
}
]);
console.log(response);
console.log(response);
})();
```

@@ -273,22 +287,23 @@

let response = await prompts([
{
type: 'text',
name: 'twitter',
message: `What's your twitter handle?`
},
{
type: 'multiselect',
name: 'color',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00' },
{ title: 'Blue', value: '#0000ff' }
],
}
]);
(async () => {
const response = await prompts([
{
type: 'text',
name: 'twitter',
message: `What's your twitter handle?`
},
{
type: 'multiselect',
name: 'color',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00' },
{ title: 'Blue', value: '#0000ff' }
],
}
]);
// => { twitter: 'terkelg', color: [ '#ff0000', '#0000ff' ] }
// => { twitter: 'terkelg', color: [ '#ff0000', '#0000ff' ] }
})();
```

@@ -324,5 +339,5 @@

{
type: prev => prev > 3 ? 'confirm' : null,
name: 'confirm',
message: (prev, values) => `Please confirm that you eat ${values.dish} times ${prev} a day?`
type: prev => prev > 3 ? 'confirm' : null,
name: 'confirm',
message: (prev, values) => `Please confirm that you eat ${values.dish} times ${prev} a day?`
}

@@ -382,6 +397,6 @@ ```

{
type: 'number',
name: 'price',
message: 'Enter price',
format: val => Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(val);
type: 'number',
name: 'price',
message: 'Enter price',
format: val => Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(val);
}

@@ -637,11 +652,11 @@ ```

{
type: 'select',
name: 'value',
message: 'Pick a color',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00', disabled: true },
{ title: 'Blue', value: '#0000ff' }
],
initial: 1
type: 'select',
name: 'value',
message: 'Pick a color',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00', disabled: true },
{ title: 'Blue', value: '#0000ff' }
],
initial: 1
}

@@ -663,3 +678,5 @@ ```

### multiselect(message, choices, [initial], [max], [hint], [warn])
> Interactive multi-select prompt.
### autocompleteMultiselect(same)
> Interactive multi-select prompt.
> Autocomplete is a searchable multiselect prompt with the same options. Useful for long lists.

@@ -674,12 +691,12 @@ Use <kbd>space</kbd> to toggle select/unselect and <kbd>up</kbd>/<kbd>down</kbd> to navigate. Use <kbd>tab</kbd> to cycle the list. You can also use <kbd>right</kbd> to select and <kbd>left</kbd> to deselect.

{
type: 'multiselect',
name: 'value',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00', disabled: true },
{ title: 'Blue', value: '#0000ff', selected: true }
],
max: 2,
hint: '- Space to select. Return to submit'
type: 'multiselect',
name: 'value',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00', disabled: true },
{ title: 'Blue', value: '#0000ff', selected: true }
],
max: 2,
hint: '- Space to select. Return to submit'
}

@@ -694,2 +711,3 @@ ```

| choices | `Array` | Array of strings or choices objects `[{ title, value, disabled }, ...]`. The choice's index in the array will be used as its value if it is not specified. |
| min | `number` | Min select - will display error |
| max | `number` | Max select |

@@ -719,12 +737,12 @@ | hint | `string` | Hint to display to the user |

{
type: 'autocomplete',
name: 'value',
message: 'Pick your favorite actor',
choices: [
{ title: 'Cage' },
{ title: 'Clooney', value: 'silver-fox' },
{ title: 'Gyllenhaal' },
{ title: 'Gibson' },
{ title: 'Grant' }
]
type: 'autocomplete',
name: 'value',
message: 'Pick your favorite actor',
choices: [
{ title: 'Cage' },
{ title: 'Clooney', value: 'silver-fox' },
{ title: 'Gyllenhaal' },
{ title: 'Gibson' },
{ title: 'Grant' }
]
}

@@ -764,7 +782,7 @@ ```

{
type: 'date',
name: 'value',
message: 'Pick a date',
initial: new Date(1997, 09, 12),
validate: date => date > Date.now() ? 'Not in the future' : true
type: 'date',
name: 'value',
message: 'Pick a date',
initial: new Date(1997, 09, 12),
validate: date => date > Date.now() ? 'Not in the future' : true
}

@@ -788,18 +806,18 @@ ```

{
months: [
'January', 'February', 'March', 'April',
'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'
],
monthsShort: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
weekdays: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday'
],
weekdaysShort: [
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]
months: [
'January', 'February', 'March', 'April',
'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December'
],
monthsShort: [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
],
weekdays: [
'Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday'
],
weekdaysShort: [
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]
}

@@ -806,0 +824,0 @@ ```

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc