Prompt Template Tag and Plugin arg validation (#673)

* Plugin arg validation, prompt tag, and some changes needed

* Version bumps
This commit is contained in:
Gregory Schier 2017-12-21 06:01:51 -08:00 committed by GitHub
parent 4e0fe5d78a
commit aba3c8ed86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 256 additions and 104 deletions

View File

@ -382,4 +382,30 @@ describe('render()', () => {
expect(err.message).toBe('unknown block tag: invalid');
}
});
it('outputs correct error path', async () => {
const template = {
foo: [{bar: '{% foo %}'}]
};
try {
await renderUtils.render(template);
fail('Should have failed to render');
} catch (err) {
expect(err.path).toBe('foo[0].bar');
}
});
it('outputs correct error path when private first node', async () => {
const template = {
_foo: {_bar: {baz: '{% foo %}'}}
};
try {
await renderUtils.render(template);
fail('Should have failed to render');
} catch (err) {
expect(err.path).toBe('_bar.baz');
}
});
});

View File

@ -12,6 +12,8 @@ import type {Environment} from '../models/environment';
export const KEEP_ON_ERROR = 'keep';
export const THROW_ON_ERROR = 'throw';
export const RENDER_PURPOSE_SEND = 'send';
export const RENDER_PURPOSE_GENERAL = 'general';
export type RenderedRequest = Request & {
cookies: Array<{name: string, value: string, disabled?: boolean}>,
@ -112,7 +114,7 @@ export async function render<T> (
// Make a deep copy so no one gets mad :)
const newObj = clone(obj);
async function next (x: any, path: string = name): Promise<any> {
async function next (x: any, path: string, first: boolean = false): Promise<any> {
if (blacklistPathRegex && path.match(blacklistPathRegex)) {
return x;
}
@ -158,21 +160,26 @@ export async function render<T> (
const keys = Object.keys(x);
for (const key of keys) {
const pathPrefix = path ? path + '.' : '';
x[key] = await next(x[key], `${pathPrefix}${key}`);
if (first && key.indexOf('_') === 0) {
x[key] = await next(x[key], path);
} else {
const pathPrefix = path ? path + '.' : '';
x[key] = await next(x[key], `${pathPrefix}${key}`);
}
}
}
return x;
}
return next(newObj);
return next(newObj, name, true);
}
export async function getRenderContext (
request: Request,
environmentId: string,
ancestors: Array<BaseModel> | null = null
ancestors: Array<BaseModel> | null = null,
purpose: string | null = null
): Promise<Object> {
if (!request) {
return {};
@ -201,21 +208,22 @@ export async function getRenderContext (
workspaceId: workspace ? workspace._id : 'n/a'
});
baseContext.getPurpose = () => purpose;
// Generate the context we need to render
const context = await buildRenderContext(
return await buildRenderContext(
ancestors,
rootEnvironment,
subEnvironment,
baseContext
);
return context;
}
export async function getRenderedRequest (
export async function getRenderedRequestAndContext (
request: Request,
environmentId: string
): Promise<RenderedRequest> {
environmentId: string,
purpose?: string
): Promise<{request: RenderedRequest, context: Object}> {
const ancestors = await db.withAncestors(request, [
models.request.type,
models.requestGroup.type,
@ -225,20 +233,17 @@ export async function getRenderedRequest (
const parentId = workspace ? workspace._id : 'n/a';
const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId);
const renderContext = await getRenderContext(request, environmentId, ancestors);
const renderContext = await getRenderContext(request, environmentId, ancestors, purpose);
// Render all request properties
const renderedRequest = await render(
request,
const renderResult = await render(
{_request: request, _cookieJar: cookieJar},
renderContext,
request.settingDisableRenderRequestBody ? /^body.*/ : null
);
// Render cookies
const renderedCookieJar = await render(
cookieJar,
renderContext
);
const renderedRequest = renderResult._request;
const renderedCookieJar = renderResult._cookieJar;
// Remove disabled params
renderedRequest.parameters = renderedRequest.parameters.filter(p => !p.disabled);
@ -260,34 +265,46 @@ export async function getRenderedRequest (
renderedRequest.url = setDefaultProtocol(renderedRequest.url);
return {
// Add the yummy cookies
// TODO: Eventually get rid of RenderedRequest type and put these elsewhere
cookieJar: renderedCookieJar,
cookies: [],
context: renderContext,
request: {
// Add the yummy cookies
// TODO: Eventually get rid of RenderedRequest type and put these elsewhere
cookieJar: renderedCookieJar,
cookies: [],
// NOTE: Flow doesn't like Object.assign, so we have to do each property manually
// for now to convert Request to RenderedRequest.
_id: renderedRequest._id,
authentication: renderedRequest.authentication,
body: renderedRequest.body,
created: renderedRequest.created,
modified: renderedRequest.modified,
description: renderedRequest.description,
headers: renderedRequest.headers,
metaSortKey: renderedRequest.metaSortKey,
method: renderedRequest.method,
name: renderedRequest.name,
parameters: renderedRequest.parameters,
parentId: renderedRequest.parentId,
settingDisableRenderRequestBody: renderedRequest.settingDisableRenderRequestBody,
settingEncodeUrl: renderedRequest.settingEncodeUrl,
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies,
type: renderedRequest.type,
url: renderedRequest.url
// NOTE: Flow doesn't like Object.assign, so we have to do each property manually
// for now to convert Request to RenderedRequest.
_id: renderedRequest._id,
authentication: renderedRequest.authentication,
body: renderedRequest.body,
created: renderedRequest.created,
modified: renderedRequest.modified,
description: renderedRequest.description,
headers: renderedRequest.headers,
metaSortKey: renderedRequest.metaSortKey,
method: renderedRequest.method,
name: renderedRequest.name,
parameters: renderedRequest.parameters,
parentId: renderedRequest.parentId,
settingDisableRenderRequestBody: renderedRequest.settingDisableRenderRequestBody,
settingEncodeUrl: renderedRequest.settingEncodeUrl,
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies,
type: renderedRequest.type,
url: renderedRequest.url
}
};
}
export async function getRenderedRequest (
request: Request,
environmentId: string,
purpose?: string
): Promise<RenderedRequest> {
const result = await getRenderedRequestAndContext(request, environmentId, purpose);
return result.request;
}
/**
* Sort the keys that may have Nunjucks last, so that other keys get
* defined first. Very important if env variables defined in same obj

View File

@ -4,7 +4,7 @@ import type {Request, RequestHeader} from '../models/request';
import type {Workspace} from '../models/workspace';
import type {Settings} from '../models/settings';
import type {RenderedRequest} from '../common/render';
import {getRenderContext, getRenderedRequest} from '../common/render';
import {getRenderedRequest, getRenderedRequestAndContext, RENDER_PURPOSE_SEND} from '../common/render';
import mkdirp from 'mkdirp';
import clone from 'clone';
import {parse as urlParse, resolve as urlResolve} from 'url';
@ -15,7 +15,7 @@ import * as electron from 'electron';
import * as models from '../models';
import {AUTH_AWS_IAM, AUTH_BASIC, AUTH_DIGEST, AUTH_NETRC, AUTH_NTLM, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion, getTempDir, STATUS_CODE_PLUGIN_ERROR} from '../common/constants';
import {delay, describeByteSize, getContentTypeHeader, getLocationHeader, getSetCookieHeaders, hasAcceptEncodingHeader, hasAcceptHeader, hasAuthHeader, hasContentTypeHeader, hasUserAgentHeader, waitForStreamToFinish} from '../common/misc';
import {setDefaultProtocol, smartEncodeUrl, buildQueryStringFromParams, joinUrlAndQueryString} from 'insomnia-url';
import {buildQueryStringFromParams, joinUrlAndQueryString, setDefaultProtocol, smartEncodeUrl} from 'insomnia-url';
import fs from 'fs';
import * as db from '../common/database';
import * as CACerts from './cacert';
@ -733,8 +733,14 @@ export async function send (
throw new Error(`Failed to find request to send for ${requestId}`);
}
const renderedRequestBeforePlugins = await getRenderedRequest(request, environmentId);
const renderedContextBeforePlugins = await getRenderContext(request, environmentId, ancestors);
const renderResult = await getRenderedRequestAndContext(
request,
environmentId,
RENDER_PURPOSE_SEND
);
const renderedRequestBeforePlugins = renderResult.request;
const renderedContextBeforePlugins = renderResult.context;
const workspaceDoc = ancestors.find(doc => doc.type === models.workspace.type);
const workspace = await models.workspace.getById(workspaceDoc ? workspaceDoc._id : 'n/a');
@ -775,8 +781,8 @@ async function _applyRequestPluginHooks (
newRenderedRequest = clone(newRenderedRequest);
const context = {
...pluginContexts.app.init(plugin),
...pluginContexts.request.init(plugin, newRenderedRequest, renderedContext)
...pluginContexts.app.init(),
...pluginContexts.request.init(newRenderedRequest, renderedContext)
};
try {
@ -795,8 +801,8 @@ async function _applyResponsePluginHooks (
): Promise<void> {
for (const {plugin, hook} of await plugins.getResponseHooks()) {
const context = {
...pluginContexts.app.init(plugin),
...pluginContexts.response.init(plugin, response)
...pluginContexts.app.init(),
...pluginContexts.response.init(response)
};
try {

View File

@ -1,22 +1,17 @@
import * as plugin from '../app';
import * as modals from '../../../ui/components/modals';
import {globalBeforeEach} from '../../../__jest__/before-each';
const PLUGIN = {
name: 'my-plugin',
version: '1.0.0',
directory: '/plugins/my-plugin',
module: {}
};
import {RENDER_PURPOSE_SEND} from '../../../common/render';
describe('init()', () => {
beforeEach(globalBeforeEach);
it('initializes correctly', async () => {
const result = plugin.init({name: PLUGIN});
const result = plugin.init();
expect(Object.keys(result)).toEqual(['app']);
expect(Object.keys(result.app).sort()).toEqual([
'alert',
'getPath',
'prompt',
'showSaveDialog'
]);
});
@ -24,18 +19,56 @@ describe('init()', () => {
describe('app.alert()', () => {
beforeEach(globalBeforeEach);
it('shows alert with message', async () => {
it('does not show alert when not sending', async () => {
modals.showAlert = jest.fn();
const result = plugin.init();
result.app.alert();
// Make sure it passes correct arguments
expect(modals.showAlert.mock.calls).toEqual([]);
});
it('shows alert with message when sending', async () => {
modals.showAlert = jest.fn().mockReturnValue('dummy-return-value');
const result = plugin.init(PLUGIN);
const result = plugin.init(RENDER_PURPOSE_SEND);
// Make sure it returns result of showAlert()
expect(result.app.alert()).toBe('dummy-return-value');
expect(result.app.alert('My message')).toBe('dummy-return-value');
expect(result.app.alert({title: 'My message'})).toBe('dummy-return-value');
// Make sure it passes correct arguments
expect(modals.showAlert.mock.calls).toEqual([
[{message: '', title: 'Plugin my-plugin'}],
[{message: 'My message', title: 'Plugin my-plugin'}]
[{}],
[{title: 'My message'}]
]);
});
});
describe('app.prompt()', () => {
beforeEach(globalBeforeEach);
it('does not show prompt when not sending', async () => {
modals.showPrompt = jest.fn();
const result = plugin.init();
result.app.prompt();
// Make sure it passes correct arguments
expect(modals.showPrompt.mock.calls).toEqual([]);
});
it('shows alert with message when sending', async () => {
modals.showPrompt = jest.fn();
const result = plugin.init(RENDER_PURPOSE_SEND);
// Make sure it returns result of showAlert()
result.app.prompt();
result.app.prompt({title: 'My message'});
// Make sure it passes correct arguments
expect(modals.showPrompt.mock.calls).toEqual([
[{cancelable: false, onComplete: expect.any(Function)}],
[{cancelable: false, onComplete: expect.any(Function), title: 'My message'}]
]);
});
});

View File

@ -2,13 +2,6 @@ import * as plugin from '../request';
import * as models from '../../../models';
import {globalBeforeEach} from '../../../__jest__/before-each';
const PLUGIN = {
name: 'my-plugin',
version: '1.0.0',
directory: '/plugins/my-plugin',
module: {}
};
const CONTEXT = {
user_key: 'my_user_key',
hello: 'world',
@ -25,7 +18,7 @@ describe('init()', () => {
});
it('initializes correctly', async () => {
const result = plugin.init(PLUGIN, await models.request.getById('req_1'), CONTEXT);
const result = plugin.init(await models.request.getById('req_1'), CONTEXT);
expect(Object.keys(result)).toEqual(['request']);
expect(Object.keys(result.request).sort()).toEqual([
'addHeader',
@ -49,7 +42,7 @@ describe('init()', () => {
});
it('fails to initialize without request', () => {
expect(() => plugin.init(PLUGIN))
expect(() => plugin.init())
.toThrowError('contexts.request initialized without request');
});
});
@ -70,7 +63,7 @@ describe('request.*', () => {
});
it('works for basic getters', async () => {
const result = plugin.init(PLUGIN, await models.request.getById('req_1'), CONTEXT);
const result = plugin.init(await models.request.getById('req_1'), CONTEXT);
expect(result.request.getId()).toBe('req_1');
expect(result.request.getName()).toBe('My Request');
expect(result.request.getUrl()).toBe('');
@ -78,7 +71,7 @@ describe('request.*', () => {
});
it('works for headers', async () => {
const result = plugin.init(PLUGIN, await models.request.getById('req_1'), CONTEXT);
const result = plugin.init(await models.request.getById('req_1'), CONTEXT);
// getHeaders()
expect(result.request.getHeaders()).toEqual([
@ -112,7 +105,7 @@ describe('request.*', () => {
const request = await models.request.getById('req_1');
request.cookies = []; // Because the plugin technically needs a RenderedRequest
const result = plugin.init(PLUGIN, request, CONTEXT);
const result = plugin.init(request, CONTEXT);
result.request.setCookie('foo', 'bar');
result.request.setCookie('foo', 'baz');
@ -123,7 +116,7 @@ describe('request.*', () => {
const request = await models.request.getById('req_1');
request.cookies = []; // Because the plugin technically needs a RenderedRequest
const result = plugin.init(PLUGIN, request, CONTEXT);
const result = plugin.init(request, CONTEXT);
// getEnvironment
expect(result.request.getEnvironment()).toEqual({

View File

@ -5,17 +5,10 @@ import fs from 'fs';
import path from 'path';
import * as models from '../../../models/index';
const PLUGIN = {
name: 'my-plugin',
version: '1.0.0',
directory: '/plugins/my-plugin',
module: {}
};
describe('init()', () => {
beforeEach(globalBeforeEach);
it('initializes correctly', async () => {
const result = plugin.init(PLUGIN, {});
const result = plugin.init({});
expect(Object.keys(result)).toEqual(['response']);
expect(Object.keys(result.response)).toEqual([
'getRequestId',
@ -31,7 +24,7 @@ describe('init()', () => {
});
it('fails to initialize without response', () => {
expect(() => plugin.init(PLUGIN))
expect(() => plugin.init())
.toThrowError('contexts.response initialized without response');
});
});
@ -51,7 +44,7 @@ describe('response.*', () => {
bytesRead: 123,
elapsedTime: 321
});
const result = plugin.init(PLUGIN, response);
const result = plugin.init(response);
expect(result.response.getRequestId()).toBe('req_1');
expect(result.response.getStatusCode()).toBe(200);
expect(result.response.getBytesRead()).toBe(123);
@ -60,7 +53,7 @@ describe('response.*', () => {
});
it('works for basic and empty response', async () => {
const result = plugin.init(PLUGIN, {});
const result = plugin.init({});
expect(result.response.getRequestId()).toBe('');
expect(result.response.getStatusCode()).toBe(0);
expect(result.response.getBytesRead()).toBe(0);
@ -76,7 +69,7 @@ describe('response.*', () => {
{name: 'set-cookie', value: 'baz=qux'}
]
};
const result = plugin.init(PLUGIN, response);
const result = plugin.init(response);
expect(result.response.getHeader('Does-Not-Exist')).toBeNull();
expect(result.response.getHeader('CONTENT-TYPE')).toBe('application/json');
expect(result.response.getHeader('set-cookie')).toEqual(['foo=bar', 'baz=qux']);

View File

@ -1,13 +1,35 @@
// @flow
import type {Plugin} from '../';
import * as electron from 'electron';
import {showAlert} from '../../ui/components/modals/index';
import {showPrompt} from '../../ui/components/modals';
import {RENDER_PURPOSE_SEND} from '../../common/render';
export function init (plugin: Plugin): {app: Object} {
export function init (renderPurpose?: string): {app: Object} {
return {
app: {
alert (message: string): Promise<void> {
return showAlert({title: `Plugin ${plugin.name}`, message: message || ''});
alert (options: {title: string, message: string}): Promise<void> {
if (renderPurpose !== RENDER_PURPOSE_SEND) {
return Promise.resolve();
}
return showAlert(options || {});
},
prompt (options: {title: string, label?: string, defaultValue?: string, submitName?: string}): Promise<string> {
options = options || {};
if (renderPurpose !== RENDER_PURPOSE_SEND) {
return Promise.resolve(options.defaultValue || '');
}
return new Promise(resolve => {
showPrompt({
...(options || {}),
cancelable: false,
onComplete (value: string) {
resolve(value);
}
});
});
},
getPath (name: string): string {
switch (name.toLowerCase()) {
@ -18,6 +40,10 @@ export function init (plugin: Plugin): {app: Object} {
}
},
async showSaveDialog (options: {defaultPath?: string} = {}): Promise<string | null> {
if (renderPurpose !== RENDER_PURPOSE_SEND) {
return Promise.resolve(null);
}
return new Promise(resolve => {
const saveOptions = {
title: 'Save File',

View File

@ -1,8 +1,7 @@
// @flow
import type {Plugin} from '../';
import {exportHAR, exportJSON, importRaw, importUri} from '../../common/import';
export function init (plugin: Plugin): {'import': Object, 'export': Object} {
export function init (): {'import': Object, 'export': Object} {
return {
'import': {
async uri (uri: string, options: {workspaceId?: string} = {}): Promise<void> {

View File

@ -1,10 +1,8 @@
// @flow
import type {Plugin} from '../';
import type {RenderedRequest} from '../../common/render';
import * as misc from '../../common/misc';
export function init (
plugin: Plugin,
renderedRequest: RenderedRequest,
renderedContext: Object
): {request: Object} {

View File

@ -1,5 +1,4 @@
// @flow
import type {Plugin} from '../';
import type {ResponseHeader} from '../../models/response';
import * as models from '../../models/index';
import {Readable} from 'stream';
@ -16,7 +15,6 @@ type MaybeResponse = {
}
export function init (
plugin: Plugin,
response: MaybeResponse,
bodyBuffer: Buffer | null = null
): {response: Object} {

View File

@ -37,6 +37,7 @@ const CORE_PLUGINS = [
'insomnia-plugin-file',
'insomnia-plugin-now',
'insomnia-plugin-uuid',
'insomnia-plugin-prompt',
'insomnia-plugin-request',
'insomnia-plugin-response'
];

View File

@ -1,5 +1,6 @@
import * as models from '../models/index';
import * as templating from './index';
import * as pluginContexts from '../plugins/context';
const EMPTY_ARG = '__EMPTY_NUNJUCKS_ARG__';
@ -60,6 +61,9 @@ export default class BaseExtension {
// Pull out the meta helper
const renderMeta = renderContext.getMeta ? renderContext.getMeta() : {};
// Pull out the purpose
const renderPurpose = renderContext.getPurpose ? renderContext.getPurpose() : null;
// Extract the rest of the args
const args = runArgs
.slice(0, runArgs.length - 1)
@ -67,6 +71,7 @@ export default class BaseExtension {
// Define a helper context with utils
const helperContext = {
...pluginContexts.app.init(renderPurpose),
context: renderContext,
meta: renderMeta,
util: {

View File

@ -82,5 +82,6 @@ export type PluginTemplateTag = {
description: string,
run: (context: PluginTemplateTagContext, ...arg: Array<any>) => Promise<any> | any,
deprecated?: boolean,
validate?: (value: any) => ?string,
priority?: number
};

View File

@ -54,7 +54,7 @@ export function render (text: string, config: Object = {}): Promise<string> {
: 'error';
const newError = new RenderError(sanitizedMsg);
newError.path = path || null;
newError.path = path || '';
newError.message = sanitizedMsg;
newError.location = {line, column};
newError.type = 'render';

View File

@ -20,6 +20,7 @@ class PromptModal extends PureComponent {
upperCase: false,
hint: null,
inputType: 'text',
cancelable: true,
hints: []
};
}
@ -66,6 +67,7 @@ class PromptModal extends PureComponent {
selectText,
upperCase,
hint,
cancelable,
inputType,
placeholder,
label,
@ -82,6 +84,7 @@ class PromptModal extends PureComponent {
defaultValue,
submitName,
selectText,
cancelable,
placeholder,
upperCase,
hint,
@ -107,7 +110,7 @@ class PromptModal extends PureComponent {
);
return (
<div type="button" key={hint} className={classes}>
<div key={hint} className={classes}>
<Button className="tall" onClick={this._handleSelectHint} value={hint}>
{hint}
</Button>
@ -131,7 +134,8 @@ class PromptModal extends PureComponent {
placeholder,
label,
upperCase,
hints
hints,
cancelable
} = this.state;
const input = (
@ -152,7 +156,7 @@ class PromptModal extends PureComponent {
}
return (
<Modal ref={this._setModalRef}>
<Modal ref={this._setModalRef} noEscape={!cancelable}>
<ModalHeader>{title}</ModalHeader>
<ModalBody className="wide">
<form onSubmit={this._handleSubmit} className="wide pad">

View File

@ -509,6 +509,12 @@ class TagEditor extends React.PureComponent<Props, State> {
typeof argDefinition.displayName === 'function'
) ? fnOrString(argDefinition.displayName, argDatas) : '';
let validationError = '';
const canValidate = argDefinition.type === 'string' || argDefinition.type === 'number';
if (canValidate && typeof argDefinition.validate === 'function') {
validationError = argDefinition.validate(strValue) || '';
}
const formControlClasses = classnames({
'form-control': true,
'form-control--thin': argDefinition.type === 'boolean',
@ -522,6 +528,7 @@ class TagEditor extends React.PureComponent<Props, State> {
{fnOrString(displayName, argDatas)}
{isVariable && <span className="faded space-left">(Variable)</span>}
{help && <HelpTooltip className="space-left">{help}</HelpTooltip>}
{validationError && <span className="font-error space-left">{validationError}</span>}
{argInputVariable || argInput}
</label>
</div>

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "1.0.5",
"version": "1.0.6",
"name": "insomnia-app",
"app": {
"name": "insomnia",
@ -108,6 +108,7 @@
"insomnia-plugin-file": "^1.0.3",
"insomnia-plugin-hash": "^1.0.3",
"insomnia-plugin-now": "^1.0.3",
"insomnia-plugin-prompt": "^1.0.1",
"insomnia-plugin-request": "^1.0.4",
"insomnia-plugin-response": "^1.0.5",
"insomnia-plugin-uuid": "^1.0.3",

View File

@ -0,0 +1,5 @@
# Insomnia Prompt Template Tag
[![Npm Version](https://img.shields.io/npm/v/insomnia-plugin-prompt.svg)](https://www.npmjs.com/package/insomnia-plugin-prompt)
This is a core Insomnia plugin.

View File

@ -0,0 +1,21 @@
module.exports.templateTags = [{
displayName: 'Prompt',
name: 'prompt',
description: 'prompt user for input',
args: [{
displayName: 'Title',
type: 'string'
}, {
displayName: 'Label',
type: 'string'
}, {
displayName: 'Default Value',
type: 'string',
help: 'This value is used to pre-populate the prompt dialog, but is ALSO used ' +
'when the app renders preview values (like the one below). This is to prevent the ' +
'prompt from displaying too frequently during general app use.'
}],
run (context, title, label, defaultValue) {
return context.app.prompt({title, label, defaultValue});
}
}];

View File

@ -0,0 +1,18 @@
{
"name": "insomnia-plugin-prompt",
"version": "1.0.1",
"author": "Gregory Schier <gschier1990@gmail.com>",
"description": "Insomnia prompt template tag",
"license": "MIT",
"repository": "https://github.com/getinsomnia/insomnia/tree/master/plugins/insomnia-plugin-prompt",
"bugs": {
"url": "https://github.com/getinsomnia/insomnia"
},
"main": "index.js",
"insomnia": {
"name": "prompt"
},
"scripts": {
"test": "node --version"
}
}