Skip to content

Commit

Permalink
chore(prompt): various enhancements and refactors (#576)
Browse files Browse the repository at this point in the history
* refactor(login): simplify with overrides

* test: address TODO

* refactor: replace onState with onCancel

I was looking through the docs and saw that onState was injected into every question in the question array and would run on every keystroke, and saw that onCancel existed and would look a lot cleaner in our prompt wrapper.

* chore: remove `enableTerminalCursor`

I'm not sure this is needed? I'm not able to get rid of the cursor anymore 🤔

* refactor: use dynamic key detection

* chore: small JS doc

* feat(prompts): top-level CI detection
  • Loading branch information
kanadgupta authored Aug 19, 2022
1 parent cc56430 commit 0b2750d
Show file tree
Hide file tree
Showing 18 changed files with 68 additions and 36 deletions.
18 changes: 17 additions & 1 deletion __tests__/cmds/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,21 @@ describe('rdme login', () => {
mock.done();
});

it.todo('should error if trying to access a project that is not yours');
it('should error if trying to access a project that is not yours', async () => {
const projectThatIsNotYours = 'unauthorized-project';
prompts.inject([email, password, projectThatIsNotYours]);
const errorResponse = {
error: 'PROJECT_NOTFOUND',
message: `The project (${projectThatIsNotYours}) can't be found.`,
suggestion: `The project is referred to as your \`subdomain\` in the dashboard when you're looking for it. If you're sure it exists, maybe you don't have access to it? You can check if it exists here: https://${projectThatIsNotYours}.readme.io.`,
help: 'If you need help, email support@readme.io',
};

const mock = getAPIMock()
.post('/api/v1/login', { email, password, project: projectThatIsNotYours })
.reply(404, errorResponse);

await expect(cmd.run({})).rejects.toStrictEqual(new APIError(errorResponse));
mock.done();
});
});
2 changes: 1 addition & 1 deletion src/cmds/categories/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default class CategoriesCreateCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { categoryType, title, key, version, preventDuplicates } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/categories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class CategoriesCommand extends Command {
}

async run(opts: CommandOptions<{}>) {
super.run(opts, true);
super.run(opts);

const { key, version } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/changelogs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class ChangelogsCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { dryRun, folder, key } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/changelogs/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default class SingleChangelogCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { dryRun, filePath, key } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/custompages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class CustomPagesCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { dryRun, folder, key } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/custompages/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class SingleCustomPageCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { dryRun, filePath, key } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/docs/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default class EditDocsCommand extends Command {
}

async run(opts: CommandOptions<Options>): Promise<undefined> {
super.run(opts, true);
super.run(opts);

const { slug, key, version } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class DocsCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { dryRun, folder, key, version } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/docs/single.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class SingleDocCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { dryRun, filePath, key, version } = opts;

Expand Down
11 changes: 4 additions & 7 deletions src/cmds/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CommandOptions } from '../lib/baseCommand';

import chalk from 'chalk';
import config from 'config';
import prompts from 'prompts';
import isEmail from 'validator/lib/isEmail';

import Command, { CommandCategories } from '../lib/baseCommand';
Expand Down Expand Up @@ -44,9 +45,9 @@ export default class LoginCommand extends Command {
async run(opts: CommandOptions<Options>) {
super.run(opts);

let { project } = opts;
prompts.override(opts);

const promptResults = await promptTerminal([
const { email, password, project, token } = await promptTerminal([
{
type: 'text',
name: 'email',
Expand All @@ -62,7 +63,7 @@ export default class LoginCommand extends Command {
message: 'What is your password?',
},
{
type: opts.project ? null : 'text',
type: 'text',
name: 'project',
message: 'What project are you logging into?',
initial: configStore.get('project'),
Expand All @@ -74,10 +75,6 @@ export default class LoginCommand extends Command {
},
]);

const { email, password, token } = promptResults;

if (promptResults.project) project = promptResults.project;

if (!project) {
return Promise.reject(new Error('No project subdomain provided. Please use `--project`.'));
}
Expand Down
2 changes: 1 addition & 1 deletion src/cmds/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default class OpenAPICommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { key, id, spec, useSpecVersion, version, workingDirectory } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/versions/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class CreateVersionCommand extends Command {
}

async run(opts: CommandOptions<VersionCreateOptions>) {
super.run(opts, true);
super.run(opts);

let versionList;
const { key, version, fork, codename, main, beta, isPublic } = opts;
Expand Down
2 changes: 1 addition & 1 deletion src/cmds/versions/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class DeleteVersionCommand extends Command {
}

async run(opts: CommandOptions<{}>) {
super.run(opts, true);
super.run(opts);

const { key, version } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/versions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default class VersionsCommand extends Command {
}

async run(opts: CommandOptions<Options>) {
super.run(opts, true);
super.run(opts);

const { key, version, raw } = opts;

Expand Down
2 changes: 1 addition & 1 deletion src/cmds/versions/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class UpdateVersionCommand extends Command {
}

async run(opts: CommandOptions<VersionUpdateOptions>) {
super.run(opts, true);
super.run(opts);

const { key, version, newVersion, codename, main, beta, isPublic, deprecated } = opts;

Expand Down
4 changes: 2 additions & 2 deletions src/lib/baseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ export default class Command {
*/
args: OptionDefinition[];

run(opts: CommandOptions<{}>, requiresAuth?: boolean): void | Promise<string> {
run(opts: CommandOptions<{}>): void | Promise<string> {
Command.debug(`command: ${this.command}`);
Command.debug(`opts: ${JSON.stringify(opts)}`);

if (requiresAuth) {
if (this.args.some(arg => arg.name === 'key')) {
if (!opts.key) {
throw new Error('No project API key provided. Please use `--key`.');
}
Expand Down
43 changes: 31 additions & 12 deletions src/lib/promptWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ciDetect from '@npmcli/ci-detect';
import prompts from 'prompts';

/**
Expand All @@ -11,29 +12,47 @@ export default async function promptTerminal<T extends string = string>(
questions: prompts.PromptObject<T> | prompts.PromptObject<T>[],
options?: prompts.Options
): Promise<prompts.Answers<T>> {
const enableTerminalCursor = () => {
process.stdout.write('\x1B[?25h');
/**
* The CTRL+C handler discussed above.
* @see {@link https://github.com/terkelg/prompts#optionsoncancel}
*/
const onCancel = () => {
process.stdout.write('\n');
process.stdout.write('Thanks for using rdme! See you soon ✌️');
process.stdout.write('\n\n');
process.exit(1);
};

const onState = (state: { aborted: boolean }) => {
if (state.aborted) {
// If we don't re-enable the terminal cursor before exiting the program, the cursor will
// remain hidden.
enableTerminalCursor();
/**
* Runs a check before every prompt renders to make sure that
* prompt is not being run in a CI environment.
*
* @todo it'd be cool if we could just throw an error here
* and have it bubble up the error to our top-level error handler
* in src/cli.ts
*/
function onRender() {
if (ciDetect() && process.env.NODE_ENV !== 'testing') {
process.stdout.write('\n');
process.stdout.write(
'Yikes! Looks like we prompted you for something in a CI environment. Are you missing an argument?'
);
process.stdout.write('\n\n');
process.stdout.write('Thanks for using rdme! See you soon ✌️');
process.stdout.write('Try running `rdme <command> --help` or get in touch at support@readme.io.');
process.stdout.write('\n\n');
process.exit(1);
}
};
}

if (Array.isArray(questions)) {
// eslint-disable-next-line no-param-reassign
questions = questions.map(question => ({ ...question, onState }));
questions = questions.map(question => ({ onRender, ...question }));
} else {
// @ts-expect-error onRender is not a documented type,
// but it definitely is a thing: https://github.com/terkelg/prompts#onrender
// eslint-disable-next-line no-param-reassign
questions.onState = onState;
questions.onRender = onRender;
}

return prompts(questions, options);
return prompts(questions, { onCancel, ...options });
}

0 comments on commit 0b2750d

Please sign in to comment.