diff --git a/.eslintrc b/.eslintrc index b1b1c5ff2..7935df591 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,6 +2,12 @@ "extends": ["@readme/eslint-config"], "root": true, "rules": { + /** + * Because our command classes have a `run` method that might not always call `this` we need to + * explicitly exclude `run` from this rule. + */ + "class-methods-use-this": ["error", { "exceptMethods": ["run"] }], + /** * This is a small rule to prevent us from using console.log() statements in our commands. * diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa88bcc45..b71df7b78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,3 @@ jobs: - name: Run tests run: npm test - env: - # `chalk` has troubles with color detection while on CI. - # https://github.com/chalk/supports-color/issues/106 - FORCE_COLOR: 1 diff --git a/.gitignore b/.gitignore index ba2a97b57..2492c9c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -node_modules -coverage +node_modules/ +coverage/ +swagger.json diff --git a/__tests__/cmds/__snapshots__/versions.test.js.snap b/__tests__/cmds/__snapshots__/versions.test.js.snap deleted file mode 100644 index 38d68adfa..000000000 --- a/__tests__/cmds/__snapshots__/versions.test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`rdme versions* rdme versions should get a specific version object if version flag provided 1`] = ` -"Version: 1.0.0 -Codename: None -Created on: 2019-06-17T22:39:56.462Z -Released on: undefined -┌───────────────┬───────────┬─────────┬───────────┐ -│ Is deprecated │ Is hidden │ Is beta │ Is stable │ -├───────────────┼───────────┼─────────┼───────────┤ -│ no │ no │ no │ yes │ -└───────────────┴───────────┴─────────┴───────────┘" -`; - -exports[`rdme versions* rdme versions should make a request to get a list of existing versions 1`] = ` -"┌─────────┬──────────┬───────────────┬───────────┬─────────┬───────────┬──────────────────────────┐ -│ Version │ Codename │ Is deprecated │ Is hidden │ Is beta │ Is stable │ Created on │ -├─────────┼──────────┼───────────────┼───────────┼─────────┼───────────┼──────────────────────────┤ -│ 1.0.0 │ None │ no │ no │ no │ yes │ 2019-06-17T22:39:56.462Z │ -├─────────┼──────────┼───────────────┼───────────┼─────────┼───────────┼──────────────────────────┤ -│ 2.0.0 │ None │ no │ no │ no │ yes │ 2019-06-17T22:39:56.462Z │ -└─────────┴──────────┴───────────────┴───────────┴─────────┴───────────┴──────────────────────────┘" -`; diff --git a/__tests__/cmds/docs.test.js b/__tests__/cmds/docs.test.js index fd1bc1376..fe14f22ba 100644 --- a/__tests__/cmds/docs.test.js +++ b/__tests__/cmds/docs.test.js @@ -8,8 +8,11 @@ const frontMatter = require('gray-matter'); const APIError = require('../../src/lib/apiError'); -const docs = require('../../src/cmds/docs'); -const docsEdit = require('../../src/cmds/docs/edit'); +const DocsCommand = require('../../src/cmds/docs'); +const DocsEditCommand = require('../../src/cmds/docs/edit'); + +const docs = new DocsCommand(); +const docsEdit = new DocsEditCommand(); const fixturesDir = `${__dirname}./../__fixtures__`; const key = 'API_KEY'; diff --git a/__tests__/cmds/login.test.js b/__tests__/cmds/login.test.js index cf11e1edd..c3b4a8bbe 100644 --- a/__tests__/cmds/login.test.js +++ b/__tests__/cmds/login.test.js @@ -1,9 +1,11 @@ const nock = require('nock'); const config = require('config'); const configStore = require('../../src/lib/configstore'); -const cmd = require('../../src/cmds/login'); +const Command = require('../../src/cmds/login'); const APIError = require('../../src/lib/apiError'); +const cmd = new Command(); + const email = 'user@example.com'; const password = '123456'; const project = 'subdomain'; @@ -33,6 +35,7 @@ describe('rdme login', () => { const mock = nock(config.get('host')).post('/api/v1/login', { email, password, project }).reply(200, { apiKey }); await expect(cmd.run({ email, password, project })).resolves.toMatchSnapshot(); + mock.done(); expect(configStore.get('apiKey')).toBe(apiKey); diff --git a/__tests__/cmds/logout.test.js b/__tests__/cmds/logout.test.js index 647984c27..44a4524b6 100644 --- a/__tests__/cmds/logout.test.js +++ b/__tests__/cmds/logout.test.js @@ -1,7 +1,8 @@ const config = require('config'); const configStore = require('../../src/lib/configstore'); -const cmd = require('../../src/cmds/logout'); -const loginCmd = require('../../src/cmds/login'); +const Command = require('../../src/cmds/logout'); + +const cmd = new Command(); describe('rdme logout', () => { it("should report the user as logged out if they aren't logged in", () => { @@ -9,7 +10,7 @@ describe('rdme logout', () => { configStore.delete('project'); return expect(cmd.run({})).resolves.toBe( - `You have logged out of ReadMe. Please use \`${config.get('cli')} ${loginCmd.command}\` to login again.` + `You have logged out of ReadMe. Please use \`${config.get('cli')} login\` to login again.` ); }); @@ -18,7 +19,7 @@ describe('rdme logout', () => { configStore.set('project', 'subdomain'); await expect(cmd.run({})).resolves.toBe( - `You have logged out of ReadMe. Please use \`${config.get('cli')} ${loginCmd.command}\` to login again.` + `You have logged out of ReadMe. Please use \`${config.get('cli')} login\` to login again.` ); expect(configStore.get('email')).toBeUndefined(); diff --git a/__tests__/cmds/open.test.js b/__tests__/cmds/open.test.js index 117a19887..4ef2297e2 100644 --- a/__tests__/cmds/open.test.js +++ b/__tests__/cmds/open.test.js @@ -1,14 +1,15 @@ const chalk = require('chalk'); const config = require('config'); const configStore = require('../../src/lib/configstore'); -const cmd = require('../../src/cmds/open'); -const loginCmd = require('../../src/cmds/login'); +const Command = require('../../src/cmds/open'); + +const cmd = new Command(); describe('rdme open', () => { it('should error if no project provided', () => { configStore.delete('project'); - return expect(cmd.run({})).rejects.toThrow(`Please login using \`${config.get('cli')} ${loginCmd.command}\`.`); + return expect(cmd.run({})).rejects.toThrow(`Please login using \`${config.get('cli')} login\`.`); }); it('should open the project', () => { diff --git a/__tests__/cmds/openapi.test.js b/__tests__/cmds/openapi.test.js index 67bef3ee5..857a0432c 100644 --- a/__tests__/cmds/openapi.test.js +++ b/__tests__/cmds/openapi.test.js @@ -3,10 +3,13 @@ const chalk = require('chalk'); const config = require('config'); const fs = require('fs'); const promptHandler = require('../../src/lib/prompts'); -const swagger = require('../../src/cmds/swagger'); -const openapi = require('../../src/cmds/openapi'); +const SwaggerCommand = require('../../src/cmds/swagger'); +const OpenAPICommand = require('../../src/cmds/openapi'); const APIError = require('../../src/lib/apiError'); +const openapi = new OpenAPICommand(); +const swagger = new SwaggerCommand(); + const key = 'API_KEY'; const id = '5aa0409b7cf527a93bfb44df'; const version = '1.0.0'; diff --git a/__tests__/cmds/validate.test.js b/__tests__/cmds/validate.test.js index c1bcb33a6..dfaa4cb0f 100644 --- a/__tests__/cmds/validate.test.js +++ b/__tests__/cmds/validate.test.js @@ -1,6 +1,8 @@ const fs = require('fs'); const chalk = require('chalk'); -const validate = require('../../src/cmds/validate'); +const Command = require('../../src/cmds/validate'); + +const validate = new Command(); const getCommandOutput = () => { return [console.info.mock.calls.join('\n\n')].filter(Boolean).join('\n\n'); diff --git a/__tests__/cmds/versions.test.js b/__tests__/cmds/versions.test.js index 9d5a61a6e..f6100855f 100644 --- a/__tests__/cmds/versions.test.js +++ b/__tests__/cmds/versions.test.js @@ -3,10 +3,10 @@ const config = require('config'); const promptHandler = require('../../src/lib/prompts'); const APIError = require('../../src/lib/apiError'); -const versions = require('../../src/cmds/versions'); -const createVersion = require('../../src/cmds/versions/create'); -const deleteVersion = require('../../src/cmds/versions/delete'); -const updateVersion = require('../../src/cmds/versions/update'); +const VersionsCommand = require('../../src/cmds/versions'); +const CreateVersionCommand = require('../../src/cmds/versions/create'); +const DeleteVersionCommand = require('../../src/cmds/versions/delete'); +const UpdateVersionCommand = require('../../src/cmds/versions/update'); const key = 'API_KEY'; const version = '1.0.0'; @@ -40,6 +40,8 @@ describe('rdme versions*', () => { afterEach(() => nock.cleanAll()); describe('rdme versions', () => { + const versions = new VersionsCommand(); + it('should error if no api key provided', () => { return expect(versions.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') @@ -52,7 +54,9 @@ describe('rdme versions*', () => { .basicAuth({ user: key }) .reply(200, [versionPayload, version2Payload]); - await expect(versions.run({ key })).resolves.toMatchSnapshot(); + const table = await versions.run({ key }); + expect(table).toContain(version); + expect(table).toContain(version2); mockRequest.done(); }); @@ -73,7 +77,9 @@ describe('rdme versions*', () => { .basicAuth({ user: key }) .reply(200, versionPayload); - await expect(versions.run({ key, version })).resolves.toMatchSnapshot(); + const table = await versions.run({ key, version }); + expect(table).toContain(version); + expect(table).not.toContain(version2); mockRequest.done(); }); @@ -90,6 +96,8 @@ describe('rdme versions*', () => { }); describe('rdme versions:create', () => { + const createVersion = new CreateVersionCommand(); + it('should error if no api key provided', () => { return createVersion.run({}).catch(err => { expect(err.message).toBe('No project API key provided. Please use `--key`.'); @@ -143,6 +151,8 @@ describe('rdme versions*', () => { }); describe('rdme versions:delete', () => { + const deleteVersion = new DeleteVersionCommand(); + it('should error if no api key provided', () => { return expect(deleteVersion.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') @@ -185,6 +195,8 @@ describe('rdme versions*', () => { }); describe('rdme versions:update', () => { + const updateVersion = new UpdateVersionCommand(); + it('should error if no api key provided', () => { return expect(updateVersion.run({})).rejects.toStrictEqual( new Error('No project API key provided. Please use `--key`.') diff --git a/__tests__/cmds/whoami.test.js b/__tests__/cmds/whoami.test.js index b6190a8da..87b663d88 100644 --- a/__tests__/cmds/whoami.test.js +++ b/__tests__/cmds/whoami.test.js @@ -1,16 +1,15 @@ const config = require('config'); const configStore = require('../../src/lib/configstore'); -const cmd = require('../../src/cmds/whoami'); -const loginCmd = require('../../src/cmds/login'); +const Command = require('../../src/cmds/whoami'); + +const cmd = new Command(); describe('rdme whoami', () => { it('should error if user is not authenticated', () => { configStore.delete('email'); configStore.delete('project'); - return expect(cmd.run({})).rejects.toStrictEqual( - new Error(`Please login using \`${config.get('cli')} ${loginCmd.command}\`.`) - ); + return expect(cmd.run({})).rejects.toStrictEqual(new Error(`Please login using \`${config.get('cli')} login\`.`)); }); it('should return the authenticated user', () => { diff --git a/__tests__/set-node-env.js b/__tests__/set-node-env.js index aa0d807e1..e22b9bf21 100644 --- a/__tests__/set-node-env.js +++ b/__tests__/set-node-env.js @@ -1,4 +1,6 @@ -// Chalk has trouble with Jest sometimes in test snapshots so we're disabling colorization here for all tests. +// The `chalk` and `colors` libraries have trouble with Jest sometimes in test snapshots so we're disabling +// colorization here for all tests. +// https://github.com/chalk/supports-color/issues/106 process.env.FORCE_COLOR = 0; process.env.NODE_ENV = 'testing'; diff --git a/src/cmds/docs/edit.js b/src/cmds/docs/edit.js index 959835437..1b94474c6 100644 --- a/src/cmds/docs/edit.js +++ b/src/cmds/docs/edit.js @@ -12,90 +12,94 @@ const writeFile = promisify(fs.writeFile); const readFile = promisify(fs.readFile); const unlink = promisify(fs.unlink); -exports.command = 'docs:edit'; -exports.usage = 'docs:edit [options]'; -exports.description = 'Edit a single file from your ReadMe project without saving locally.'; -exports.category = 'docs'; -exports.position = 2; +module.exports = class EditDocsCommand { + constructor() { + this.command = 'docs:edit'; + this.usage = 'docs:edit [options]'; + this.description = 'Edit a single file from your ReadMe project without saving locally.'; + this.category = 'docs'; + this.position = 2; -exports.hiddenArgs = ['slug']; -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'version', - type: String, - description: 'Project version', - }, - { - name: 'slug', - type: String, - defaultOption: true, - }, -]; + this.hiddenArgs = ['slug']; + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'version', + type: String, + description: 'Project version', + }, + { + name: 'slug', + type: String, + defaultOption: true, + }, + ]; + } -exports.run = async function (opts) { - const { slug, key, version } = opts; + async run(opts) { + const { slug, key, version } = opts; - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); - } + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } - if (!slug) { - return Promise.reject(new Error(`No slug provided. Usage \`${config.get('cli')} ${exports.usage}\`.`)); - } + if (!slug) { + return Promise.reject(new Error(`No slug provided. Usage \`${config.get('cli')} ${this.usage}\`.`)); + } - const selectedVersion = await getProjectVersion(version, key, true).catch(e => { - return Promise.reject(e); - }); + const selectedVersion = await getProjectVersion(version, key, true).catch(e => { + return Promise.reject(e); + }); - const filename = `${slug}.md`; + const filename = `${slug}.md`; - const existingDoc = await fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'get', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - Accept: 'application/json', - }), - }).then(res => handleRes(res)); + const existingDoc = await fetch(`${config.get('host')}/api/v1/docs/${slug}`, { + method: 'get', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + Accept: 'application/json', + }), + }).then(res => handleRes(res)); - await writeFile(filename, existingDoc.body); + await writeFile(filename, existingDoc.body); - return new Promise((resolve, reject) => { - (opts.mockEditor || editor)(filename, async code => { - if (code !== 0) return reject(new Error('Non zero exit code from $EDITOR')); - const updatedDoc = await readFile(filename, 'utf8'); + return new Promise((resolve, reject) => { + (opts.mockEditor || editor)(filename, async code => { + if (code !== 0) return reject(new Error('Non zero exit code from $EDITOR')); + const updatedDoc = await readFile(filename, 'utf8'); - return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'put', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify( - Object.assign(existingDoc, { - body: updatedDoc, - }) - ), - }) - .then(res => res.json()) - .then(async res => { - // The reason we aren't using our handleRes() function here is - // because we need to use the `reject` function from - // the Promise that's wrapping this function. - if (res.error) { - return reject(new APIError(res)); - } - console.info(`Doc successfully updated. Cleaning up local file.`); - await unlink(filename); - // Normally we should resolve with a value that is logged to the console, - // but since we need to wait for the temporary file to be removed, - // it's okay to resolve the promise with no value. - return resolve(); - }); + return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { + method: 'put', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + 'Content-Type': 'application/json', + }), + body: JSON.stringify( + Object.assign(existingDoc, { + body: updatedDoc, + }) + ), + }) + .then(res => res.json()) + .then(async res => { + // The reason we aren't using our handleRes() function here is + // because we need to use the `reject` function from + // the Promise that's wrapping this function. + if (res.error) { + return reject(new APIError(res)); + } + console.info(`Doc successfully updated. Cleaning up local file.`); + await unlink(filename); + // Normally we should resolve with a value that is logged to the console, + // but since we need to wait for the temporary file to be removed, + // it's okay to resolve the promise with no value. + return resolve(); + }); + }); }); - }); + } }; diff --git a/src/cmds/docs/index.js b/src/cmds/docs/index.js index 9a20bd5b4..91a1f7632 100644 --- a/src/cmds/docs/index.js +++ b/src/cmds/docs/index.js @@ -12,135 +12,139 @@ const fetch = require('node-fetch'); const readFile = promisify(fs.readFile); -exports.command = 'docs'; -exports.usage = 'docs [options]'; -exports.description = 'Sync a folder of markdown files to your ReadMe project.'; -exports.category = 'docs'; -exports.position = 1; - -exports.hiddenArgs = ['folder']; -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'version', - type: String, - description: 'Project version', - }, - { - name: 'folder', - type: String, - defaultOption: true, - }, -]; - -exports.run = async function (opts) { - const { folder, key, version } = opts; - - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); +module.exports = class DocsCommand { + constructor() { + this.command = 'docs'; + this.usage = 'docs [options]'; + this.description = 'Sync a folder of markdown files to your ReadMe project.'; + this.category = 'docs'; + this.position = 1; + + this.hiddenArgs = ['folder']; + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'version', + type: String, + description: 'Project version', + }, + { + name: 'folder', + type: String, + defaultOption: true, + }, + ]; } - if (!folder) { - return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${exports.usage}\`.`)); - } - - // TODO: should we allow version selection at all here? - // Let's revisit this once we re-evaluate our category logic in the API. - // Ideally we should ignore this parameter entirely if the category is included. - const selectedVersion = await getProjectVersion(version, key, false); - - // Find the files to sync - const readdirRecursive = folderToSearch => { - const filesInFolder = fs.readdirSync(folderToSearch, { withFileTypes: true }); - const files = filesInFolder - .filter(fileHandle => fileHandle.isFile()) - .map(fileHandle => path.join(folderToSearch, fileHandle.name)); - const folders = filesInFolder.filter(fileHandle => fileHandle.isDirectory()); - const subFiles = [].concat( - ...folders.map(fileHandle => readdirRecursive(path.join(folderToSearch, fileHandle.name))) - ); - return [...files, ...subFiles]; - }; + async run(opts) { + const { folder, key, version } = opts; - // Strip out non-markdown files - const files = readdirRecursive(folder).filter(file => file.endsWith('.md') || file.endsWith('.markdown')); - if (!files.length) { - return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`)); - } + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } - function createDoc(slug, file, hash, err) { - if (err.error !== 'DOC_NOTFOUND') return Promise.reject(err); - - return fetch(`${config.get('host')}/api/v1/docs`, { - method: 'post', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - slug, - body: file.content, - ...file.data, - lastUpdatedHash: hash, - }), - }).then(res => handleRes(res)); - } + if (!folder) { + return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${this.usage}\`.`)); + } - function updateDoc(slug, file, hash, existingDoc) { - if (hash === existingDoc.lastUpdatedHash) { - return `\`${slug}\` was not updated because there were no changes.`; + // TODO: should we allow version selection at all here? + // Let's revisit this once we re-evaluate our category logic in the API. + // Ideally we should ignore this parameter entirely if the category is included. + const selectedVersion = await getProjectVersion(version, key, false); + + // Find the files to sync + const readdirRecursive = folderToSearch => { + const filesInFolder = fs.readdirSync(folderToSearch, { withFileTypes: true }); + const files = filesInFolder + .filter(fileHandle => fileHandle.isFile()) + .map(fileHandle => path.join(folderToSearch, fileHandle.name)); + const folders = filesInFolder.filter(fileHandle => fileHandle.isDirectory()); + const subFiles = [].concat( + ...folders.map(fileHandle => readdirRecursive(path.join(folderToSearch, fileHandle.name))) + ); + return [...files, ...subFiles]; + }; + + // Strip out non-markdown files + const files = readdirRecursive(folder).filter(file => file.endsWith('.md') || file.endsWith('.markdown')); + if (!files.length) { + return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`)); } - return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'put', - headers: cleanHeaders(key, { - 'x-readme-version': selectedVersion, - 'Content-Type': 'application/json', - }), - body: JSON.stringify( - Object.assign(existingDoc, { + function createDoc(slug, file, hash, err) { + if (err.error !== 'DOC_NOTFOUND') return Promise.reject(err); + + return fetch(`${config.get('host')}/api/v1/docs`, { + method: 'post', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + slug, body: file.content, ...file.data, lastUpdatedHash: hash, - }) - ), - }).then(res => handleRes(res)); - } - - const updatedDocs = await Promise.all( - files.map(async filename => { - const file = await readFile(filename, 'utf8'); - const matter = frontMatter(file); + }), + }).then(res => handleRes(res)); + } - // Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug. - const slug = matter.data.slug || path.basename(filename).replace(path.extname(filename), '').toLowerCase(); - const hash = crypto.createHash('sha1').update(file).digest('hex'); + function updateDoc(slug, file, hash, existingDoc) { + if (hash === existingDoc.lastUpdatedHash) { + return `\`${slug}\` was not updated because there were no changes.`; + } return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { - method: 'get', + method: 'put', headers: cleanHeaders(key, { 'x-readme-version': selectedVersion, - Accept: 'application/json', + 'Content-Type': 'application/json', }), - }) - .then(res => res.json()) - .then(res => { - if (res.error) { - return createDoc(slug, matter, hash, res); - } - return updateDoc(slug, matter, hash, res); + body: JSON.stringify( + Object.assign(existingDoc, { + body: file.content, + ...file.data, + lastUpdatedHash: hash, + }) + ), + }).then(res => handleRes(res)); + } + + const updatedDocs = await Promise.all( + files.map(async filename => { + const file = await readFile(filename, 'utf8'); + const matter = frontMatter(file); + + // Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug. + const slug = matter.data.slug || path.basename(filename).replace(path.extname(filename), '').toLowerCase(); + const hash = crypto.createHash('sha1').update(file).digest('hex'); + + return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { + method: 'get', + headers: cleanHeaders(key, { + 'x-readme-version': selectedVersion, + Accept: 'application/json', + }), }) - .catch(err => { - // eslint-disable-next-line no-param-reassign - err.message = `Error uploading ${chalk.underline(filename)}:\n\n${err.message}`; - throw err; - }); - }) - ); - - return updatedDocs; + .then(res => res.json()) + .then(res => { + if (res.error) { + return createDoc(slug, matter, hash, res); + } + return updateDoc(slug, matter, hash, res); + }) + .catch(err => { + // eslint-disable-next-line no-param-reassign + err.message = `Error uploading ${chalk.underline(filename)}:\n\n${err.message}`; + throw err; + }); + }) + ); + + return updatedDocs; + } }; diff --git a/src/cmds/login.js b/src/cmds/login.js index 9a69fd8bb..50a3ec0a3 100644 --- a/src/cmds/login.js +++ b/src/cmds/login.js @@ -9,68 +9,72 @@ const fetch = require('node-fetch'); const testing = process.env.NODE_ENV === 'testing'; -exports.command = 'login'; -exports.usage = 'login [options]'; -exports.description = 'Login to a ReadMe project.'; -exports.category = 'admin'; -exports.position = 1; +module.exports = class LoginCommand { + constructor() { + this.command = 'login'; + this.usage = 'login [options]'; + this.description = 'Login to a ReadMe project.'; + this.category = 'admin'; + this.position = 1; -exports.args = [ - { - name: 'project', - type: String, - description: 'Project subdomain', - }, - { - name: '2fa', - type: Boolean, - description: 'Prompt for a 2FA token', - }, -]; + this.args = [ + { + name: 'project', + type: String, + description: 'Project subdomain', + }, + { + name: '2fa', + type: Boolean, + description: 'Prompt for a 2FA token', + }, + ]; + } -/* istanbul ignore next */ -async function getCredentials(opts) { - return { - email: await read({ prompt: 'Email:', default: configStore.get('email') }), - password: await read({ prompt: 'Password:', silent: true }), - project: opts.project || (await read({ prompt: 'Project subdomain:', default: configStore.get('project') })), - token: opts['2fa'] && (await read({ prompt: '2fa token:' })), - }; -} + async run(opts) { + let { email, password, project, token } = opts; -exports.run = async function (opts) { - let { email, password, project, token } = opts; + /* istanbul ignore next */ + async function getCredentials() { + return { + email: await read({ prompt: 'Email:', default: configStore.get('email') }), + password: await read({ prompt: 'Password:', silent: true }), + project: opts.project || (await read({ prompt: 'Project subdomain:', default: configStore.get('project') })), + token: opts['2fa'] && (await read({ prompt: '2fa token:' })), + }; + } - // We only want to prompt for input outside of the test environment - /* istanbul ignore next */ - if (!testing) { - ({ email, password, project, token } = await getCredentials(opts)); - } + // We only want to prompt for input outside of the test environment + /* istanbul ignore next */ + if (!testing) { + ({ email, password, project, token } = await getCredentials()); + } - if (!project) { - return Promise.reject(new Error('No project subdomain provided. Please use `--project`.')); - } + if (!project) { + return Promise.reject(new Error('No project subdomain provided. Please use `--project`.')); + } - if (!isEmail(email)) { - return Promise.reject(new Error('You must provide a valid email address.')); - } + if (!isEmail(email)) { + return Promise.reject(new Error('You must provide a valid email address.')); + } - return fetch(`${config.get('host')}/api/v1/login`, { - method: 'post', - headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email, - password, - project, - token, - }), - }) - .then(handleRes) - .then(res => { - configStore.set('apiKey', res.apiKey); - configStore.set('email', email); - configStore.set('project', project); + return fetch(`${config.get('host')}/api/v1/login`, { + method: 'post', + headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + password, + project, + token, + }), + }) + .then(handleRes) + .then(res => { + configStore.set('apiKey', res.apiKey); + configStore.set('email', email); + configStore.set('project', project); - return `Successfully logged in as ${chalk.green(email)} to the ${chalk.blue(project)} project.`; - }); + return `Successfully logged in as ${chalk.green(email)} to the ${chalk.blue(project)} project.`; + }); + } }; diff --git a/src/cmds/logout.js b/src/cmds/logout.js index c64335cea..e13b02b9d 100644 --- a/src/cmds/logout.js +++ b/src/cmds/logout.js @@ -1,21 +1,22 @@ const config = require('config'); const configStore = require('../lib/configstore'); -const loginCmd = require('./login'); -exports.command = 'logout'; -exports.usage = 'logout'; -exports.description = 'Logs the currently authenticated user out of ReadMe.'; -exports.category = 'admin'; -exports.position = 2; +module.exports = class LogoutCommand { + constructor() { + this.command = 'logout'; + this.usage = 'logout'; + this.description = 'Logs the currently authenticated user out of ReadMe.'; + this.category = 'admin'; + this.position = 2; -exports.args = []; - -exports.run = async () => { - if (configStore.has('email') && configStore.has('project')) { - configStore.clear(); + this.args = []; } - return Promise.resolve( - `You have logged out of ReadMe. Please use \`${config.get('cli')} ${loginCmd.command}\` to login again.` - ); + async run() { + if (configStore.has('email') && configStore.has('project')) { + configStore.clear(); + } + + return Promise.resolve(`You have logged out of ReadMe. Please use \`${config.get('cli')} login\` to login again.`); + } }; diff --git a/src/cmds/oas.js b/src/cmds/oas.js index 051a227de..c1f1eaa7a 100644 --- a/src/cmds/oas.js +++ b/src/cmds/oas.js @@ -1,24 +1,28 @@ const { spawn } = require('child_process'); const path = require('path'); -exports.command = 'oas'; -exports.usage = 'oas'; -exports.description = 'Helpful OpenAPI generation tooling.'; -exports.category = 'utilities'; -exports.position = 1; +module.exports = class OASCommand { + constructor() { + this.command = 'oas'; + this.usage = 'oas'; + this.description = 'Helpful OpenAPI generation tooling.'; + this.category = 'utilities'; + this.position = 1; -exports.args = []; + this.args = []; + } -exports.run = function () { - const cp = spawn(path.join(__dirname, '..', '..', 'node_modules', '.bin', 'oas'), process.argv.slice(3), { - stdio: 'inherit', - }); + async run() { + const cp = spawn(path.join(__dirname, '..', '..', 'node_modules', '.bin', 'oas'), process.argv.slice(3), { + stdio: 'inherit', + }); - return new Promise((resolve, reject) => { - cp.on('close', code => { - if (code && code > 0) return reject(); + return new Promise((resolve, reject) => { + cp.on('close', code => { + if (code && code > 0) return reject(); - return resolve(); + return resolve(); + }); }); - }); + } }; diff --git a/src/cmds/open.js b/src/cmds/open.js index 13a6aecde..c14702554 100644 --- a/src/cmds/open.js +++ b/src/cmds/open.js @@ -2,26 +2,29 @@ const chalk = require('chalk'); const config = require('config'); const open = require('open'); const configStore = require('../lib/configstore'); -const loginCmd = require('./login'); -exports.command = 'open'; -exports.usage = 'open'; -exports.description = 'Open your current ReadMe project in the browser.'; -exports.category = 'utilities'; -exports.position = 2; +module.exports = class OpenCommand { + constructor() { + this.command = 'open'; + this.usage = 'open'; + this.description = 'Open your current ReadMe project in the browser.'; + this.category = 'utilities'; + this.position = 2; -exports.args = []; - -exports.run = function (opts) { - const project = configStore.get('project'); - if (!project) { - return Promise.reject(new Error(`Please login using \`${config.get('cli')} ${loginCmd.command}\`.`)); + this.args = []; } - const url = config.get('hub').replace('{project}', project); + async run(opts) { + const project = configStore.get('project'); + if (!project) { + return Promise.reject(new Error(`Please login using \`${config.get('cli')} login\`.`)); + } + + const url = config.get('hub').replace('{project}', project); - return (opts.mockOpen || open)(url, { - wait: false, - url: true, - }).then(() => Promise.resolve(`Opening ${chalk.green(url)} in your browser...`)); + return (opts.mockOpen || open)(url, { + wait: false, + url: true, + }).then(() => Promise.resolve(`Opening ${chalk.green(url)} in your browser...`)); + } }; diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js index 3fc9aa9c4..73c12f784 100644 --- a/src/cmds/openapi.js +++ b/src/cmds/openapi.js @@ -12,215 +12,219 @@ const FormData = require('form-data'); const parse = require('parse-link-header'); const { file: tmpFile } = require('tmp-promise'); -exports.command = 'openapi'; -exports.usage = 'openapi [file] [options]'; -exports.description = 'Upload, or resync, your OpenAPI/Swagger definition to ReadMe.'; -exports.category = 'apis'; -exports.position = 1; - -exports.hiddenArgs = ['token', 'spec']; -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'id', - type: String, - description: `Unique identifier for your API definition. Use this if you're re-uploading an existing API definition`, - }, - { - name: 'token', - type: String, - description: 'Project token. Deprecated, please use `--key` instead', - }, - { - name: 'version', - type: String, - description: 'Project version', - }, - { - name: 'spec', - type: String, - defaultOption: true, - }, -]; - -exports.run = async function (opts) { - const { spec, version } = opts; - let { key, id } = opts; - let selectedVersion; - let isUpdate; - - if (!key && opts.token) { - console.warn( - chalk.yellow('⚠️ Warning! The `--token` option has been deprecated. Please use `--key` and `--id` instead.') - ); - - [key, id] = opts.token.split('-'); +module.exports = class OpenAPICommand { + constructor() { + this.command = 'openapi'; + this.usage = 'openapi [file] [options]'; + this.description = 'Upload, or resync, your OpenAPI/Swagger definition to ReadMe.'; + this.category = 'apis'; + this.position = 1; + + this.hiddenArgs = ['token', 'spec']; + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'id', + type: String, + description: `Unique identifier for your API definition. Use this if you're re-uploading an existing API definition`, + }, + { + name: 'token', + type: String, + description: 'Project token. Deprecated, please use `--key` instead', + }, + { + name: 'version', + type: String, + description: 'Project version', + }, + { + name: 'spec', + type: String, + defaultOption: true, + }, + ]; } - if (version && id) { - console.warn( - chalk.yellow( - `⚠️ Warning! We'll be using the version associated with the \`--${ - opts.token ? 'token' : 'id' - }\` option, so the \`--version\` option will be ignored.` - ) - ); - } + async run(opts) { + const { spec, version } = opts; + let { key, id } = opts; + let selectedVersion; + let isUpdate; - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); - } + if (!key && opts.token) { + console.warn( + chalk.yellow('⚠️ Warning! The `--token` option has been deprecated. Please use `--key` and `--id` instead.') + ); + + [key, id] = opts.token.split('-'); + } - async function callApi(specPath, versionCleaned) { - // @todo Tailor messaging to what is actually being handled here. If the user is uploading a Swagger file, never mention that they uploaded/updated an OpenAPI file. - - async function success(data) { - const message = !isUpdate - ? "You've successfully uploaded a new OpenAPI file to your ReadMe project!" - : "You've successfully updated an OpenAPI file on your ReadMe project!"; - - const body = await data.json(); - - return Promise.resolve( - [ - message, - '', - `\t${chalk.green(`${data.headers.get('location')}`)}`, - '', - 'To update your OpenAPI or Swagger definition, run the following:', - '', - // eslint-disable-next-line no-underscore-dangle - `\t${chalk.green(`rdme openapi FILE --key=${key} --id=${body._id}`)}`, - ].join('\n') + if (version && id) { + console.warn( + chalk.yellow( + `⚠️ Warning! We'll be using the version associated with the \`--${ + opts.token ? 'token' : 'id' + }\` option, so the \`--version\` option will be ignored.` + ) ); } - async function error(err) { - try { - const parsedError = await err.json(); - return Promise.reject(new APIError(parsedError)); - } catch (e) { - if (e.message.includes('Unexpected token < in JSON')) { - return Promise.reject( - new Error( - "We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks." - ) - ); - } + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } - return Promise.reject(new Error('There was an error uploading!')); + async function callApi(specPath, versionCleaned) { + // @todo Tailor messaging to what is actually being handled here. If the user is uploading a Swagger file, never mention that they uploaded/updated an OpenAPI file. + + async function success(data) { + const message = !isUpdate + ? "You've successfully uploaded a new OpenAPI file to your ReadMe project!" + : "You've successfully updated an OpenAPI file on your ReadMe project!"; + + const body = await data.json(); + + return Promise.resolve( + [ + message, + '', + `\t${chalk.green(`${data.headers.get('location')}`)}`, + '', + 'To update your OpenAPI or Swagger definition, run the following:', + '', + // eslint-disable-next-line no-underscore-dangle + `\t${chalk.green(`rdme openapi FILE --key=${key} --id=${body._id}`)}`, + ].join('\n') + ); } - } - let bundledSpec; - const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); - await oas.validate(false); - await oas.bundle().then(res => { - bundledSpec = JSON.stringify(res); - }); + async function error(err) { + try { + const parsedError = await err.json(); + return Promise.reject(new APIError(parsedError)); + } catch (e) { + if (e.message.includes('Unexpected token < in JSON')) { + return Promise.reject( + new Error( + "We're sorry, your upload request timed out. Please try again or split your file up into smaller chunks." + ) + ); + } + + return Promise.reject(new Error('There was an error uploading!')); + } + } - // Create a temporary file to write the bundled spec to, - // which we will then stream into the form data body - const { path } = await tmpFile({ prefix: 'rdme-openapi-', postfix: '.json' }); - await fs.writeFileSync(path, bundledSpec); - const stream = fs.createReadStream(path); - - const formData = new FormData(); - formData.append('spec', stream); - - const options = { - headers: cleanHeaders(key, { - 'x-readme-version': versionCleaned, - 'x-readme-source': 'cli', - Accept: 'application/json', - }), - body: formData, - }; - - function createSpec() { - options.method = 'post'; - return fetch(`${config.get('host')}/api/v1/api-specification`, options).then(res => { - if (res.ok) return success(res); - return error(res); + let bundledSpec; + const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); + await oas.validate(false); + await oas.bundle().then(res => { + bundledSpec = JSON.stringify(res); }); - } - function updateSpec(specId) { - isUpdate = true; - options.method = 'put'; - return fetch(`${config.get('host')}/api/v1/api-specification/${specId}`, options).then(res => { - if (res.ok) return success(res); - return error(res); - }); - } + // Create a temporary file to write the bundled spec to, + // which we will then stream into the form data body + const { path } = await tmpFile({ prefix: 'rdme-openapi-', postfix: '.json' }); + await fs.writeFileSync(path, bundledSpec); + const stream = fs.createReadStream(path); + + const formData = new FormData(); + formData.append('spec', stream); - /* - Create a new OAS file in Readme: - - Enter flow if user does not pass an id as cli arg - - Check to see if any existing files exist with a specific version - - If none exist, default to creating a new instance of a spec - - If found, prompt user to either create a new spec or update an existing one - */ - - function getSpecs(url) { - return fetch(`${config.get('host')}${url}`, { - method: 'get', + const options = { headers: cleanHeaders(key, { 'x-readme-version': versionCleaned, + 'x-readme-source': 'cli', + Accept: 'application/json', }), - }); - } + body: formData, + }; + + function createSpec() { + options.method = 'post'; + return fetch(`${config.get('host')}/api/v1/api-specification`, options).then(res => { + if (res.ok) return success(res); + return error(res); + }); + } - if (!id) { - const apiSettings = await getSpecs(`/api/v1/api-specification`); + function updateSpec(specId) { + isUpdate = true; + options.method = 'put'; + return fetch(`${config.get('host')}/api/v1/api-specification/${specId}`, options).then(res => { + if (res.ok) return success(res); + return error(res); + }); + } + + /* + Create a new OAS file in Readme: + - Enter flow if user does not pass an id as cli arg + - Check to see if any existing files exist with a specific version + - If none exist, default to creating a new instance of a spec + - If found, prompt user to either create a new spec or update an existing one + */ + + function getSpecs(url) { + return fetch(`${config.get('host')}${url}`, { + method: 'get', + headers: cleanHeaders(key, { + 'x-readme-version': versionCleaned, + }), + }); + } - const totalPages = Math.ceil(apiSettings.headers.get('x-total-count') / 10); - const parsedDocs = parse(apiSettings.headers.get('link')); + if (!id) { + const apiSettings = await getSpecs(`/api/v1/api-specification`); - const apiSettingsBody = await apiSettings.json(); - if (!apiSettingsBody.length) return createSpec(); + const totalPages = Math.ceil(apiSettings.headers.get('x-total-count') / 10); + const parsedDocs = parse(apiSettings.headers.get('link')); - const { option } = await prompt(promptOpts.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs)); - if (!option) return null; - return option === 'create' ? createSpec() : updateSpec(option); + const apiSettingsBody = await apiSettings.json(); + if (!apiSettingsBody.length) return createSpec(); + + const { option } = await prompt(promptOpts.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs)); + if (!option) return null; + return option === 'create' ? createSpec() : updateSpec(option); + } + + /* + Update an existing OAS file in Readme: + - Enter flow if user passes an id as cli arg + */ + return updateSpec(id); } - /* - Update an existing OAS file in Readme: - - Enter flow if user passes an id as cli arg - */ - return updateSpec(id); - } + if (!id) { + selectedVersion = await getProjectVersion(version, key, true); + } - if (!id) { - selectedVersion = await getProjectVersion(version, key, true); - } + if (spec) { + return callApi(spec, selectedVersion); + } - if (spec) { - return callApi(spec, selectedVersion); - } + // If the user didn't supply an API specification, let's try to locate what they've got, and upload + // that. If they don't have any, let's let the user know how they can get one going. + return new Promise((resolve, reject) => { + ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { + if (!fs.existsSync(file)) { + return; + } - // If the user didn't supply an API specification, let's try to locate what they've got, and upload - // that. If they don't have any, let's let the user know how they can get one going. - return new Promise((resolve, reject) => { - ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { - if (!fs.existsSync(file)) { - return; - } + console.info(chalk.yellow(`We found ${file} and are attempting to upload it.`)); + resolve(callApi(file, selectedVersion)); + }); - console.info(chalk.yellow(`We found ${file} and are attempting to upload it.`)); - resolve(callApi(file, selectedVersion)); + reject( + new Error( + "We couldn't find an OpenAPI or Swagger definition.\n\n" + + 'Run `rdme openapi ./path/to/api/definition` to upload an existing definition or `rdme oas init` to create a fresh one!' + ) + ); }); - - reject( - new Error( - "We couldn't find an OpenAPI or Swagger definition.\n\n" + - 'Run `rdme openapi ./path/to/api/definition` to upload an existing definition or `rdme oas init` to create a fresh one!' - ) - ); - }); + } }; diff --git a/src/cmds/swagger.js b/src/cmds/swagger.js index 06d5e56a6..2bfc4dd28 100644 --- a/src/cmds/swagger.js +++ b/src/cmds/swagger.js @@ -1,16 +1,18 @@ const chalk = require('chalk'); -const openapi = require('./openapi'); +const OpenAPICommand = require('./openapi'); -exports.command = 'swagger'; -exports.usage = 'swagger [file] [options]'; -exports.description = 'Alias for `rdme openapi`. [deprecated]'; -exports.category = openapi.category; -exports.position = openapi.position + 1; +module.exports = class SwaggerCommand extends OpenAPICommand { + constructor() { + super(); -exports.hiddenArgs = openapi.hiddenArgs; -exports.args = openapi.args; + this.command = 'swagger'; + this.usage = 'swagger [file] [options]'; + this.description = 'Alias for `rdme openapi`. [deprecated]'; + this.position += 1; + } -exports.run = async function (opts) { - console.warn(chalk.yellow('⚠️ Warning! `rdme swagger` has been deprecated. Please use `rdme openapi` instead.')); - return openapi.run(opts); + async run(opts) { + console.warn(chalk.yellow('⚠️ Warning! `rdme swagger` has been deprecated. Please use `rdme openapi` instead.')); + return super.run(opts); + } }; diff --git a/src/cmds/validate.js b/src/cmds/validate.js index 10b93b1cc..18a98ac0d 100644 --- a/src/cmds/validate.js +++ b/src/cmds/validate.js @@ -2,60 +2,64 @@ const chalk = require('chalk'); const fs = require('fs'); const OASNormalize = require('oas-normalize'); -exports.command = 'validate'; -exports.usage = 'validate [file] [options]'; -exports.description = 'Validate your OpenAPI/Swagger definition.'; -exports.category = 'apis'; -exports.position = 2; - -exports.hiddenArgs = ['spec']; -exports.args = [ - { - name: 'spec', - type: String, - defaultOption: true, - }, -]; - -exports.run = async function (opts) { - const { spec } = opts; - - async function validateSpec(specPath) { - const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); - - return oas - .validate(false) - .then(api => { - if (api.swagger) { - return Promise.resolve(chalk.green(`${specPath} is a valid Swagger API definition!`)); - } - return Promise.resolve(chalk.green(`${specPath} is a valid OpenAPI API definition!`)); - }) - .catch(err => { - return Promise.reject(new Error(err.message)); - }); - } +module.exports = class ValidateCommand { + constructor() { + this.command = 'validate'; + this.usage = 'validate [file] [options]'; + this.description = 'Validate your OpenAPI/Swagger definition.'; + this.category = 'apis'; + this.position = 2; - if (spec) { - return validateSpec(spec); + this.hiddenArgs = ['spec']; + this.args = [ + { + name: 'spec', + type: String, + defaultOption: true, + }, + ]; } - // If the user didn't supply an API specification, let's try to locate what they've got, and validate that. If they - // don't have any, let's let the user know how they can get one going. - return new Promise((resolve, reject) => { - ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { - if (!fs.existsSync(file)) { - return; - } + async run(opts) { + const { spec } = opts; - console.info(chalk.yellow(`We found ${file} and are attempting to validate it.`)); - resolve(validateSpec(file)); - }); + async function validateSpec(specPath) { + const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); + + return oas + .validate(false) + .then(api => { + if (api.swagger) { + return Promise.resolve(chalk.green(`${specPath} is a valid Swagger API definition!`)); + } + return Promise.resolve(chalk.green(`${specPath} is a valid OpenAPI API definition!`)); + }) + .catch(err => { + return Promise.reject(new Error(err.message)); + }); + } + + if (spec) { + return validateSpec(spec); + } - reject( - new Error( - "We couldn't find an OpenAPI or Swagger definition.\n\nIf you need help creating one run `rdme oas init`!" - ) - ); - }); + // If the user didn't supply an API specification, let's try to locate what they've got, and validate that. If they + // don't have any, let's let the user know how they can get one going. + return new Promise((resolve, reject) => { + ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { + if (!fs.existsSync(file)) { + return; + } + + console.info(chalk.yellow(`We found ${file} and are attempting to validate it.`)); + resolve(validateSpec(file)); + }); + + reject( + new Error( + "We couldn't find an OpenAPI or Swagger definition.\n\nIf you need help creating one run `rdme oas init`!" + ) + ); + }); + } }; diff --git a/src/cmds/versions/create.js b/src/cmds/versions/create.js index 1c83c1290..940bf9c31 100644 --- a/src/cmds/versions/create.js +++ b/src/cmds/versions/create.js @@ -2,104 +2,104 @@ const config = require('config'); const semver = require('semver'); const { prompt } = require('enquirer'); const promptOpts = require('../../lib/prompts'); -const APIError = require('../../lib/apiError'); const { cleanHeaders } = require('../../lib/cleanHeaders'); const { handleRes } = require('../../lib/handleRes'); const fetch = require('node-fetch'); -exports.command = 'versions:create'; -exports.usage = 'versions:create --version= [options]'; -exports.description = 'Create a new version for your project.'; -exports.category = 'versions'; -exports.position = 2; +module.exports = class CreateVersionCommand { + constructor() { + this.command = 'versions:create'; + this.usage = 'versions:create --version= [options]'; + this.description = 'Create a new version for your project.'; + this.category = 'versions'; + this.position = 2; -exports.hiddenArgs = ['version']; -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'version', - type: String, - defaultOption: true, - }, - { - name: 'fork', - type: String, - description: "The semantic version which you'd like to fork from.", - }, - { - name: 'codename', - type: String, - description: 'The codename, or nickname, for a particular version.', - }, - { - name: 'main', - type: String, - description: 'Should this version be the primary (default) version for your project?', - }, - { - name: 'beta', - type: String, - description: 'Is this version in beta?', - }, - { - name: 'isPublic', - type: String, - description: 'Would you like to make this version public? Any primary version must be public.', - }, -]; - -exports.run = async function (opts) { - let versionList; - const { key, version, codename, fork, main, beta, isPublic } = opts; - - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + this.hiddenArgs = ['version']; + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'version', + type: String, + defaultOption: true, + }, + { + name: 'fork', + type: String, + description: "The semantic version which you'd like to fork from.", + }, + { + name: 'codename', + type: String, + description: 'The codename, or nickname, for a particular version.', + }, + { + name: 'main', + type: String, + description: 'Should this version be the primary (default) version for your project?', + }, + { + name: 'beta', + type: String, + description: 'Is this version in beta?', + }, + { + name: 'isPublic', + type: String, + description: 'Would you like to make this version public? Any primary version must be public.', + }, + ]; } - if (!version || !semver.valid(semver.coerce(version))) { - return Promise.reject( - new Error(`Please specify a semantic version. See \`${config.get('cli')} help ${exports.command}\` for help.`) - ); - } + async run(opts) { + let versionList; + const { key, version, codename, fork, main, beta, isPublic } = opts; - if (!fork) { - versionList = await fetch(`${config.get('host')}/api/v1/version`, { - method: 'get', - headers: cleanHeaders(key), - }).then(res => handleRes(res)); - } + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } - const versionPrompt = promptOpts.createVersionPrompt(versionList || [{}], { - newVersion: version, - ...opts, - }); + if (!version || !semver.valid(semver.coerce(version))) { + return Promise.reject( + new Error(`Please specify a semantic version. See \`${config.get('cli')} help ${this.command}\` for help.`) + ); + } - const promptResponse = await prompt(versionPrompt); + if (!fork) { + versionList = await fetch(`${config.get('host')}/api/v1/version`, { + method: 'get', + headers: cleanHeaders(key), + }).then(res => handleRes(res)); + } - return fetch(`${config.get('host')}/api/v1/version`, { - method: 'post', - headers: cleanHeaders(key, { - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - version, - codename: codename || '', - is_stable: main === 'true' || promptResponse.is_stable, - is_beta: beta === 'true' || promptResponse.is_beta, - from: fork || promptResponse.from, - is_hidden: promptResponse.is_stable ? false : !(isPublic === 'true' || promptResponse.is_hidden), - }), - }) - .then(res => res.json()) - .then(res => { - if (res.error) { - return Promise.reject(new APIError(res)); - } - return Promise.resolve(`Version ${version} created successfully.`); + const versionPrompt = promptOpts.createVersionPrompt(versionList || [{}], { + newVersion: version, + ...opts, }); + + const promptResponse = await prompt(versionPrompt); + + return fetch(`${config.get('host')}/api/v1/version`, { + method: 'post', + headers: cleanHeaders(key, { + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + version, + codename: codename || '', + is_stable: main === 'true' || promptResponse.is_stable, + is_beta: beta === 'true' || promptResponse.is_beta, + from: fork || promptResponse.from, + is_hidden: promptResponse.is_stable ? false : !(isPublic === 'true' || promptResponse.is_hidden), + }), + }) + .then(handleRes) + .then(() => { + return Promise.resolve(`Version ${version} created successfully.`); + }); + } }; diff --git a/src/cmds/versions/delete.js b/src/cmds/versions/delete.js index 231b8d242..599e4f01f 100644 --- a/src/cmds/versions/delete.js +++ b/src/cmds/versions/delete.js @@ -4,43 +4,47 @@ const { cleanHeaders } = require('../../lib/cleanHeaders'); const { handleRes } = require('../../lib/handleRes'); const fetch = require('node-fetch'); -exports.command = 'versions:delete'; -exports.usage = 'versions:delete --version= [options]'; -exports.description = 'Delete a version associated with your ReadMe project.'; -exports.category = 'versions'; -exports.position = 4; +module.exports = class DeleteVersionCommand { + constructor() { + this.command = 'versions:delete'; + this.usage = 'versions:delete --version= [options]'; + this.description = 'Delete a version associated with your ReadMe project.'; + this.category = 'versions'; + this.position = 4; -exports.hiddenArgs = ['version']; -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'version', - type: String, - defaultOption: true, - }, -]; - -exports.run = async function (opts) { - const { key, version } = opts; - - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + this.hiddenArgs = ['version']; + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'version', + type: String, + defaultOption: true, + }, + ]; } - const selectedVersion = await getProjectVersion(version, key, false).catch(e => { - return Promise.reject(e); - }); + async run(opts) { + const { key, version } = opts; + + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } - return fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { - method: 'delete', - headers: cleanHeaders(key), - }) - .then(handleRes) - .then(() => { - return Promise.resolve(`Version ${selectedVersion} deleted successfully.`); + const selectedVersion = await getProjectVersion(version, key, false).catch(e => { + return Promise.reject(e); }); + + return fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { + method: 'delete', + headers: cleanHeaders(key), + }) + .then(handleRes) + .then(() => { + return Promise.resolve(`Version ${selectedVersion} deleted successfully.`); + }); + } }; diff --git a/src/cmds/versions/index.js b/src/cmds/versions/index.js index a4dcaaa59..22cbbb9f2 100644 --- a/src/cmds/versions/index.js +++ b/src/cmds/versions/index.js @@ -1,125 +1,129 @@ const chalk = require('chalk'); const Table = require('cli-table'); const config = require('config'); -const versionsCreate = require('./create'); +const CreateVersionCmd = require('./create'); const { cleanHeaders } = require('../../lib/cleanHeaders'); const fetch = require('node-fetch'); const { handleRes } = require('../../lib/handleRes'); -exports.command = 'versions'; -exports.usage = 'versions [options]'; -exports.description = 'List versions available in your project or get a version by SemVer (https://semver.org/).'; -exports.category = 'versions'; -exports.position = 1; - -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'version', - type: String, - description: 'A specific project version to view', - }, - { - name: 'raw', - type: Boolean, - description: 'Return raw output from the API instead of in a "pretty" format.', - }, -]; - -const getVersionsAsTable = versions => { - const table = new Table({ - head: [ - chalk.bold('Version'), - chalk.bold('Codename'), - chalk.bold('Is deprecated'), - chalk.bold('Is hidden'), - chalk.bold('Is beta'), - chalk.bold('Is stable'), - chalk.bold('Created on'), - ], - }); - - versions.forEach(v => { - table.push([ - v.version, - v.codename || 'None', - v.is_deprecated ? 'yes' : 'no', - v.is_hidden ? 'yes' : 'no', - v.is_beta ? 'yes' : 'no', - v.is_stable ? 'yes' : 'no', - v.createdAt, - ]); - }); +module.exports = class VersionsCommand { + constructor() { + this.command = 'versions'; + this.usage = 'versions [options]'; + this.description = 'List versions available in your project or get a version by SemVer (https://semver.org/).'; + this.category = 'versions'; + this.position = 1; + + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'version', + type: String, + description: 'A specific project version to view', + }, + { + name: 'raw', + type: Boolean, + description: 'Return raw output from the API instead of in a "pretty" format.', + }, + ]; + } - return table.toString(); -}; + static getVersionsAsTable(versions) { + const table = new Table({ + head: [ + chalk.bold('Version'), + chalk.bold('Codename'), + chalk.bold('Is deprecated'), + chalk.bold('Is hidden'), + chalk.bold('Is beta'), + chalk.bold('Is stable'), + chalk.bold('Created on'), + ], + }); -const getVersionFormatted = version => { - const output = [ - `${chalk.bold('Version:')} ${version.version}`, - `${chalk.bold('Codename:')} ${version.codename || 'None'}`, - `${chalk.bold('Created on:')} ${version.createdAt}`, - `${chalk.bold('Released on:')} ${version.releaseDate}`, - ]; + versions.forEach(v => { + table.push([ + v.version, + v.codename || 'None', + v.is_deprecated ? 'yes' : 'no', + v.is_hidden ? 'yes' : 'no', + v.is_beta ? 'yes' : 'no', + v.is_stable ? 'yes' : 'no', + v.createdAt, + ]); + }); - const table = new Table({ - head: [chalk.bold('Is deprecated'), chalk.bold('Is hidden'), chalk.bold('Is beta'), chalk.bold('Is stable')], - }); + return table.toString(); + } - table.push([ - version.is_deprecated ? 'yes' : 'no', - version.is_hidden ? 'yes' : 'no', - version.is_beta ? 'yes' : 'no', - version.is_stable ? 'yes' : 'no', - ]); + static getVersionFormatted(version) { + const output = [ + `${chalk.bold('Version:')} ${version.version}`, + `${chalk.bold('Codename:')} ${version.codename || 'None'}`, + `${chalk.bold('Created on:')} ${version.createdAt}`, + `${chalk.bold('Released on:')} ${version.releaseDate}`, + ]; - output.push(table.toString()); + const table = new Table({ + head: [chalk.bold('Is deprecated'), chalk.bold('Is hidden'), chalk.bold('Is beta'), chalk.bold('Is stable')], + }); - return output.join('\n'); -}; + table.push([ + version.is_deprecated ? 'yes' : 'no', + version.is_hidden ? 'yes' : 'no', + version.is_beta ? 'yes' : 'no', + version.is_stable ? 'yes' : 'no', + ]); -exports.run = function (opts) { - const { key, version, raw } = opts; + output.push(table.toString()); - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + return output.join('\n'); } - const uri = version ? `${config.get('host')}/api/v1/version/${version}` : `${config.get('host')}/api/v1/version`; - - return fetch(uri, { - method: 'get', - headers: cleanHeaders(key), - }) - .then(handleRes) - .then(data => { - if (raw) { - return Promise.resolve(JSON.stringify(data, null, 2)); - } - - let versions = data; - if (!Array.isArray(data)) { - versions = [data]; - } - - if (!versions.length) { - return Promise.reject( - new Error( - `Sorry, you haven't created any versions yet! See \`${config.get('cli')} help ${ - versionsCreate.command - }\` for commands on how to do that.` - ) - ); - } - - if (version === undefined) { - return Promise.resolve(getVersionsAsTable(versions)); - } - - return Promise.resolve(getVersionFormatted(versions[0])); - }); + async run(opts) { + const { key, version, raw } = opts; + + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } + + const uri = version ? `${config.get('host')}/api/v1/version/${version}` : `${config.get('host')}/api/v1/version`; + + return fetch(uri, { + method: 'get', + headers: cleanHeaders(key), + }) + .then(handleRes) + .then(data => { + if (raw) { + return Promise.resolve(JSON.stringify(data, null, 2)); + } + + let versions = data; + if (!Array.isArray(data)) { + versions = [data]; + } + + if (!versions.length) { + return Promise.reject( + new Error( + `Sorry, you haven't created any versions yet! See \`${config.get('cli')} help ${ + new CreateVersionCmd().command + }\` for commands on how to do that.` + ) + ); + } + + if (version === undefined) { + return Promise.resolve(VersionsCommand.getVersionsAsTable(versions)); + } + + return Promise.resolve(VersionsCommand.getVersionFormatted(versions[0])); + }); + } }; diff --git a/src/cmds/versions/update.js b/src/cmds/versions/update.js index e151bfa01..71b44223c 100644 --- a/src/cmds/versions/update.js +++ b/src/cmds/versions/update.js @@ -6,80 +6,84 @@ const { getProjectVersion } = require('../../lib/versionSelect'); const fetch = require('node-fetch'); const { handleRes } = require('../../lib/handleRes'); -exports.command = 'versions:update'; -exports.usage = 'versions:update --version= [options]'; -exports.description = 'Update an existing version for your project.'; -exports.category = 'versions'; -exports.position = 3; +module.exports = class UpdateVersionCommand { + constructor() { + this.command = 'versions:update'; + this.usage = 'versions:update --version= [options]'; + this.description = 'Update an existing version for your project.'; + this.category = 'versions'; + this.position = 3; -exports.args = [ - { - name: 'key', - type: String, - description: 'Project API key', - }, - { - name: 'version', - type: String, - description: 'Project version', - }, - { - name: 'codename', - type: String, - description: 'The codename, or nickname, for a particular version.', - }, - { - name: 'main', - type: String, - description: 'Should this version be the primary (default) version for your project?', - }, - { - name: 'beta', - type: String, - description: 'Is this version in beta?', - }, - { - name: 'isPublic', - type: String, - description: 'Would you like to make this version public? Any primary version must be public.', - }, -]; + this.args = [ + { + name: 'key', + type: String, + description: 'Project API key', + }, + { + name: 'version', + type: String, + description: 'Project version', + }, + { + name: 'codename', + type: String, + description: 'The codename, or nickname, for a particular version.', + }, + { + name: 'main', + type: String, + description: 'Should this version be the primary (default) version for your project?', + }, + { + name: 'beta', + type: String, + description: 'Is this version in beta?', + }, + { + name: 'isPublic', + type: String, + description: 'Would you like to make this version public? Any primary version must be public.', + }, + ]; + } -exports.run = async function (opts) { - const { key, version, codename, newVersion, main, beta, isPublic, deprecated } = opts; + async run(opts) { + const { key, version, codename, newVersion, main, beta, isPublic, deprecated } = opts; - if (!key) { - return Promise.reject(new Error('No project API key provided. Please use `--key`.')); - } + if (!key) { + return Promise.reject(new Error('No project API key provided. Please use `--key`.')); + } - const selectedVersion = await getProjectVersion(version, key, false).catch(e => { - return Promise.reject(e); - }); + const selectedVersion = await getProjectVersion(version, key, false).catch(e => { + return Promise.reject(e); + }); - const foundVersion = await fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { - method: 'get', - headers: cleanHeaders(key), - }).then(res => handleRes(res)); + const foundVersion = await fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { + method: 'get', + headers: cleanHeaders(key), + }).then(res => handleRes(res)); - const promptResponse = await prompt(promptOpts.createVersionPrompt([{}], opts, foundVersion)); + const promptResponse = await prompt(promptOpts.createVersionPrompt([{}], opts, foundVersion)); - return fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { - method: 'put', - headers: cleanHeaders(key, { - Accept: 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - codename: codename || '', - version: newVersion || promptResponse.newVersion, - is_stable: foundVersion.is_stable || main === 'true' || promptResponse.is_stable, - is_beta: beta === 'true' || promptResponse.is_beta, - is_deprecated: deprecated || promptResponse.is_deprecated, - is_hidden: promptResponse.is_stable ? false : !(isPublic === 'true' || promptResponse.is_hidden), - }), - }) - .then(handleRes) - .then(() => { - return Promise.resolve(`Version ${selectedVersion} updated successfully.`); - }); + return fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { + method: 'put', + headers: cleanHeaders(key, { + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + codename: codename || '', + version: newVersion || promptResponse.newVersion, + is_stable: foundVersion.is_stable || main === 'true' || promptResponse.is_stable, + is_beta: beta === 'true' || promptResponse.is_beta, + is_deprecated: deprecated || promptResponse.is_deprecated, + is_hidden: promptResponse.is_stable ? false : !(isPublic === 'true' || promptResponse.is_hidden), + }), + }) + .then(handleRes) + .then(() => { + return Promise.resolve(`Version ${selectedVersion} updated successfully.`); + }); + } }; diff --git a/src/cmds/whoami.js b/src/cmds/whoami.js index 3659c32ee..bae588549 100644 --- a/src/cmds/whoami.js +++ b/src/cmds/whoami.js @@ -1,24 +1,27 @@ const chalk = require('chalk'); const config = require('config'); const configStore = require('../lib/configstore'); -const loginCmd = require('./login'); -exports.command = 'whoami'; -exports.usage = 'whoami'; -exports.description = 'Displays the current user and project authenticated with ReadMe.'; -exports.category = 'admin'; -exports.position = 3; +module.exports = class WhoAmICommand { + constructor() { + this.command = 'whoami'; + this.usage = 'whoami'; + this.description = 'Displays the current user and project authenticated with ReadMe.'; + this.category = 'admin'; + this.position = 3; -exports.args = []; - -exports.run = () => { - if (!configStore.has('email') || !configStore.has('project')) { - return Promise.reject(new Error(`Please login using \`${config.get('cli')} ${loginCmd.command}\`.`)); + this.args = []; } - return Promise.resolve( - `You are currently logged in as ${chalk.green(configStore.get('email'))} to the ${chalk.blue( - configStore.get('project') - )} project.` - ); + async run() { + if (!configStore.has('email') || !configStore.has('project')) { + return Promise.reject(new Error(`Please login using \`${config.get('cli')} login\`.`)); + } + + return Promise.resolve( + `You are currently logged in as ${chalk.green(configStore.get('email'))} to the ${chalk.blue( + configStore.get('project') + )} project.` + ); + } }; diff --git a/src/lib/commands.js b/src/lib/commands.js index 65e8f416f..6d36edd2b 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -11,7 +11,8 @@ exports.load = cmd => { const file = path.join(__dirname, '../cmds', command, subcommand); try { // eslint-disable-next-line global-require, import/no-dynamic-require - return require(file); + const Command = require(file); + return new Command(); } catch (e) { throw new Error('Command not found.'); } @@ -54,11 +55,11 @@ exports.list = () => { files.forEach(file => { // eslint-disable-next-line global-require, import/no-dynamic-require - const command = require(file); + const Command = require(file); commands.push({ file, - command, + command: new Command(), }); });