diff --git a/__tests__/cmds/login.test.ts b/__tests__/cmds/login.test.ts index 4cb8ff4e9..df3473ba0 100644 --- a/__tests__/cmds/login.test.ts +++ b/__tests__/cmds/login.test.ts @@ -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(); + }); }); diff --git a/src/cmds/categories/create.ts b/src/cmds/categories/create.ts index b395fabc7..d9dd96b7b 100644 --- a/src/cmds/categories/create.ts +++ b/src/cmds/categories/create.ts @@ -58,7 +58,7 @@ export default class CategoriesCreateCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { categoryType, title, key, version, preventDuplicates } = opts; diff --git a/src/cmds/categories/index.ts b/src/cmds/categories/index.ts index d4e78cdce..77eec1da5 100644 --- a/src/cmds/categories/index.ts +++ b/src/cmds/categories/index.ts @@ -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; diff --git a/src/cmds/changelogs/index.ts b/src/cmds/changelogs/index.ts index 9338d0e8c..05dab2bf4 100644 --- a/src/cmds/changelogs/index.ts +++ b/src/cmds/changelogs/index.ts @@ -43,7 +43,7 @@ export default class ChangelogsCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { dryRun, folder, key } = opts; diff --git a/src/cmds/changelogs/single.ts b/src/cmds/changelogs/single.ts index 1e56c38c0..7d1059e0d 100644 --- a/src/cmds/changelogs/single.ts +++ b/src/cmds/changelogs/single.ts @@ -42,7 +42,7 @@ export default class SingleChangelogCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { dryRun, filePath, key } = opts; diff --git a/src/cmds/custompages/index.ts b/src/cmds/custompages/index.ts index 9e78add83..556be0b04 100644 --- a/src/cmds/custompages/index.ts +++ b/src/cmds/custompages/index.ts @@ -43,7 +43,7 @@ export default class CustomPagesCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { dryRun, folder, key } = opts; diff --git a/src/cmds/custompages/single.ts b/src/cmds/custompages/single.ts index 59c1af163..f1c6165fa 100644 --- a/src/cmds/custompages/single.ts +++ b/src/cmds/custompages/single.ts @@ -41,7 +41,7 @@ export default class SingleCustomPageCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { dryRun, filePath, key } = opts; diff --git a/src/cmds/docs/edit.ts b/src/cmds/docs/edit.ts index 8a85ea826..d8917b702 100644 --- a/src/cmds/docs/edit.ts +++ b/src/cmds/docs/edit.ts @@ -49,7 +49,7 @@ export default class EditDocsCommand extends Command { } async run(opts: CommandOptions): Promise { - super.run(opts, true); + super.run(opts); const { slug, key, version } = opts; diff --git a/src/cmds/docs/index.ts b/src/cmds/docs/index.ts index c86058ae8..0a8f529a3 100644 --- a/src/cmds/docs/index.ts +++ b/src/cmds/docs/index.ts @@ -45,7 +45,7 @@ export default class DocsCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { dryRun, folder, key, version } = opts; diff --git a/src/cmds/docs/single.ts b/src/cmds/docs/single.ts index 722a5a00a..43462b08c 100644 --- a/src/cmds/docs/single.ts +++ b/src/cmds/docs/single.ts @@ -44,7 +44,7 @@ export default class SingleDocCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { dryRun, filePath, key, version } = opts; diff --git a/src/cmds/login.ts b/src/cmds/login.ts index 17bebb69b..179f2e1ec 100644 --- a/src/cmds/login.ts +++ b/src/cmds/login.ts @@ -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'; @@ -44,9 +45,9 @@ export default class LoginCommand extends Command { async run(opts: CommandOptions) { super.run(opts); - let { project } = opts; + prompts.override(opts); - const promptResults = await promptTerminal([ + const { email, password, project, token } = await promptTerminal([ { type: 'text', name: 'email', @@ -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'), @@ -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`.')); } diff --git a/src/cmds/openapi/index.ts b/src/cmds/openapi/index.ts index 87f6cae61..b2a7df832 100644 --- a/src/cmds/openapi/index.ts +++ b/src/cmds/openapi/index.ts @@ -68,7 +68,7 @@ export default class OpenAPICommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { key, id, spec, useSpecVersion, version, workingDirectory } = opts; diff --git a/src/cmds/versions/create.ts b/src/cmds/versions/create.ts index 447dee35d..99df7eea7 100644 --- a/src/cmds/versions/create.ts +++ b/src/cmds/versions/create.ts @@ -50,7 +50,7 @@ export default class CreateVersionCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); let versionList; const { key, version, fork, codename, main, beta, isPublic } = opts; diff --git a/src/cmds/versions/delete.ts b/src/cmds/versions/delete.ts index 1c4bfdb92..0bdb512d3 100644 --- a/src/cmds/versions/delete.ts +++ b/src/cmds/versions/delete.ts @@ -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; diff --git a/src/cmds/versions/index.ts b/src/cmds/versions/index.ts index ffc3bb4da..d4fae94c2 100644 --- a/src/cmds/versions/index.ts +++ b/src/cmds/versions/index.ts @@ -106,7 +106,7 @@ export default class VersionsCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { key, version, raw } = opts; diff --git a/src/cmds/versions/update.ts b/src/cmds/versions/update.ts index b722b3848..5176c13a2 100644 --- a/src/cmds/versions/update.ts +++ b/src/cmds/versions/update.ts @@ -44,7 +44,7 @@ export default class UpdateVersionCommand extends Command { } async run(opts: CommandOptions) { - super.run(opts, true); + super.run(opts); const { key, version, newVersion, codename, main, beta, isPublic, deprecated } = opts; diff --git a/src/lib/baseCommand.ts b/src/lib/baseCommand.ts index 40546dea4..26755ce58 100644 --- a/src/lib/baseCommand.ts +++ b/src/lib/baseCommand.ts @@ -71,11 +71,11 @@ export default class Command { */ args: OptionDefinition[]; - run(opts: CommandOptions<{}>, requiresAuth?: boolean): void | Promise { + run(opts: CommandOptions<{}>): void | Promise { 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`.'); } diff --git a/src/lib/promptWrapper.ts b/src/lib/promptWrapper.ts index a5c7f0ed0..81836fd15 100644 --- a/src/lib/promptWrapper.ts +++ b/src/lib/promptWrapper.ts @@ -1,3 +1,4 @@ +import ciDetect from '@npmcli/ci-detect'; import prompts from 'prompts'; /** @@ -11,29 +12,47 @@ export default async function promptTerminal( questions: prompts.PromptObject | prompts.PromptObject[], options?: prompts.Options ): Promise> { - 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 --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 }); }