Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌺 Quality of life improvements #49

Merged
merged 30 commits into from
Aug 2, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e30935b
Cleaning up `--help`.
erunion Jul 19, 2019
931e14e
Adding Owlbert to --help
erunion Jul 19, 2019
3d28b0f
Constructing a help engine for subcommands.
erunion Jul 20, 2019
2012d38
Migrating the oas command into the new framework.
erunion Jul 20, 2019
6e21154
Migrating the open command over and pouring some sugar on it.
erunion Jul 20, 2019
242e62c
Migrating the login command over.
erunion Jul 20, 2019
3e0b8cb
Migrating the docs commands over.
erunion Jul 20, 2019
a1e8e34
Migrating the version commands over.
erunion Jul 21, 2019
7199c2a
Fixing broken command unit tests.
erunion Jul 21, 2019
494c713
Running Prettier on everything.
erunion Jul 21, 2019
e088aea
Fixing broken unit tests.
erunion Jul 21, 2019
45d1955
Boosting code coverage.
erunion Jul 22, 2019
c568791
Adding support for `rdme help <command>`.
erunion Jul 22, 2019
4cafbc3
Cleaning up the main help screen.
erunion Jul 23, 2019
325eb8c
Surfacing related commands on command help screens.
erunion Jul 23, 2019
90b473a
Fixing some typos in code comments.
erunion Jul 23, 2019
74425c3
Preferring readme.com over readme.io
erunion Jul 25, 2019
c4cde32
Re-adding supported arguments to the versions commands.
erunion Jul 25, 2019
ef8fd5e
Fixing broken unit tests.
erunion Jul 26, 2019
ee0f349
Using a better table library for `rdme versions`.
erunion Jul 26, 2019
9dac2c0
Running prettier on everything.
erunion Jul 26, 2019
5dcfdc3
Fixing a broken versions command test.
erunion Jul 26, 2019
6d3b0dc
Expanding our builds to run on Node 12
erunion Jul 26, 2019
b0bd08f
Reformatting the output of the versions command to show more data.
erunion Jul 30, 2019
dbc27b0
Running Prettier on everything.
erunion Jul 30, 2019
f810a48
Aligning the tagline with the program name.
erunion Aug 1, 2019
18dca78
Cleaning up the README file.
erunion Aug 2, 2019
7ab97c5
Removing a couple more duplicate `--key` arguments in the readme.
erunion Aug 2, 2019
7d764b3
A few more readme tweaks.
erunion Aug 2, 2019
84f819d
Fixing a typo in the readme.
erunion Aug 2, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* [Swagger](#swagger)
* [Docs](#docs)
* [Versions](#versions)
* [Opening A Project Spec](#open)
* [Opening a Project](#open)
* [Future](#future)

### About `rdme`
Expand Down
108 changes: 90 additions & 18 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable no-underscore-dangle */
const cliArgs = require('command-line-args');
const path = require('path');
// We have to do this otherwise `require('config')` loads
// from the cwd where the user is running `rdme` which
Expand All @@ -15,34 +17,104 @@ process.env.NODE_CONFIG_DIR = configDir;

const { version } = require('./package.json');
const configStore = require('./lib/configstore');
const help = require('./lib/help');
const commands = require('./lib/commands');

function load(command = '', subcommand = '') {
const file = path.join(__dirname, 'lib', command, subcommand);
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
return require(file);
} catch (e) {
const error = new Error('Command not found.');
error.description = `Type ${`${config.cli} help`.yellow} to see all commands`;
throw error;
/**
* @param {Array} processArgv - An array of arguments from the current process. Can be used to mock
* fake CLI calls.
* @return {Promise}
*/
module.exports = processArgv => {
const mainArgs = [
{ name: 'help', alias: 'h', type: Boolean, description: 'Display this usage guide' },
{
name: 'version',
alias: 'v',
type: Boolean,
description: `Show the current ${config.cli} version`,
},
{ name: 'command', type: String, defaultOption: true },
];

const argv = cliArgs(mainArgs, { partial: true, argv: processArgv });
const cmd = argv.command || false;

// Add support for `-V` as an additional `--version` alias.
if (typeof argv._unknown !== 'undefined') {
if (argv._unknown.indexOf('-V') !== -1) {
argv.version = true;
}
}
}

module.exports = function(cmd, args, opts = {}) {
if (opts.version && (!cmd || cmd === 'help')) return Promise.resolve(version);
if (argv.version && (!cmd || cmd === 'help')) return Promise.resolve(version);

let command = cmd || '';
let subcommand;

if (command.includes(':')) {
[command, subcommand] = cmd.split(':');
if (!command) {
command = 'help';
}

const optsWithStoredKey = Object.assign({}, { key: configStore.get('apiKey') }, opts);
if (command === 'help') {
argv.help = true;
}

try {
return load(opts.help ? 'help' : command, subcommand).run({ args, opts: optsWithStoredKey });
let cmdArgv;
let bin;

// Handling for `rdme help` and `rdme help <command>` cases.
if (command === 'help') {
if ((argv._unknown || []).length === 0) {
return Promise.resolve(help.globalUsage(mainArgs));
}

if (argv._unknown.indexOf('-H') !== -1) {
return Promise.resolve(help.globalUsage(mainArgs));
}

cmdArgv = cliArgs([{ name: 'subcommand', type: String, defaultOption: true }], {
argv: argv._unknown,
});
if (!cmdArgv.subcommand) {
return Promise.resolve(help.globalUsage(mainArgs));
}

bin = commands.load(cmdArgv.subcommand);
return Promise.resolve(help.commandUsage(bin));
}

bin = commands.load(command);

// Handling for `rdme <command> --help`.
if (argv.help) {
return Promise.resolve(help.commandUsage(bin));
}

try {
cmdArgv = cliArgs(bin.args, { argv: argv._unknown || [] });
} catch (e) {
// If we have a command that has its own `--version` argument to accept data, that argument,
// if supplied in the `--version VERSION_STRING` format instead of `--version=VERSION_STRING`,
// will collide with the global version argument because their types differ and the argument
// parser gets confused.
//
// Instead of failing out to the user with an undecipherable "Unknown value: ..." error, let's
// try to parse their request again but a tad less eager.
if (e.name !== 'UNKNOWN_VALUE' || (e.name === 'UNKNOWN_VALUE' && !argv.version)) {
throw e;
}

cmdArgv = cliArgs(bin.args, { partial: true, argv: processArgv.slice(1) });
}

cmdArgv = Object.assign({}, { key: configStore.get('apiKey') }, cmdArgv);

return bin.run(cmdArgv);
} catch (e) {
if (e.message === 'Command not found.') {
e.description = `Type \`${`${config.cli} help`.yellow}\` ${`to see all commands`.red}`;
}

return Promise.reject(e);
}
};
43 changes: 30 additions & 13 deletions lib/docs/edit.js β†’ cmds/docs/edit.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const request = require('request-promise-native');

const config = require('config');
const fs = require('fs');
const editor = require('editor');
Expand All @@ -9,29 +8,47 @@ const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const unlink = promisify(fs.unlink);

exports.desc = 'Edit a single file from your ReadMe project without saving locally';
exports.category = 'services';
exports.command = 'docs:edit';
exports.usage = 'docs:edit <slug>';
exports.description = 'Edit a single file from your ReadMe project without saving locally.';
exports.category = 'docs';
exports.weight = 4;
exports.action = 'docs:edit';

exports.run = async function({ args, opts }) {
const { key, version } = opts;
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,
},
];

exports.run = async function(opts) {
const { slug, key, version } = opts;

if (!key) {
return Promise.reject(new Error('No api key provided. Please use --key'));
return Promise.reject(new Error('No project API key provided. Please use `--key`.'));
}

if (!version) {
return Promise.reject(new Error('No version provided. Please use --version'));
return Promise.reject(new Error('No project version provided. Please use `--version`.'));
}

if (!args[0]) {
return Promise.reject(new Error('No slug provided. Usage `rdme docs:edit <slug>`'));
if (!slug) {
return Promise.reject(new Error(`No slug provided. Usage \`${config.cli} ${exports.usage}\`.`));
}

const slug = args[0];
const filename = `${slug}.md`;

const options = {
auth: { user: key },
headers: {
Expand Down Expand Up @@ -67,7 +84,7 @@ exports.run = async function({ args, opts }) {
...options,
})
.then(async () => {
console.log('Doc successfully updated. Cleaning up local file');
console.log(`Doc successfully updated. Cleaning up local file.`);
await unlink(filename);
return resolve();
})
Expand Down
49 changes: 37 additions & 12 deletions lib/docs/index.js β†’ cmds/docs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,50 @@ const { promisify } = require('util');

const readFile = promisify(fs.readFile);

exports.desc = 'Sync a folder of markdown files to your ReadMe project';
exports.category = 'services';
exports.command = 'docs';
exports.usage = 'docs <folder>';
exports.description = 'Sync a folder of markdown files to your ReadMe project.';
exports.category = 'docs';
exports.weight = 3;
exports.action = 'docs';

exports.run = function({ args, opts }) {
const { key, version } = opts;
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 = function(opts) {
const { folder, key, version } = opts;

if (!key) {
return Promise.reject(new Error('No api key provided. Please use --key'));
return Promise.reject(new Error('No project API key provided. Please use `--key`.'));
}

if (!version) {
return Promise.reject(new Error('No version provided. Please use --version'));
return Promise.reject(new Error('No project version provided. Please use `--version`.'));
}

if (!args[0]) {
return Promise.reject(new Error('No folder provided. Usage `rdme docs <folder>`'));
if (!folder) {
return Promise.reject(
new Error(`No folder provided. Usage \`${config.cli} ${exports.usage}\`.`),
);
}

const files = fs
.readdirSync(args[0])
.readdirSync(folder)
.filter(file => file.endsWith('.md') || file.endsWith('.markdown'));

const options = {
Expand Down Expand Up @@ -76,7 +98,7 @@ exports.run = function({ args, opts }) {

return Promise.all(
files.map(async filename => {
const file = await readFile(path.join(args[0], filename), 'utf8');
const file = await readFile(path.join(folder, filename), 'utf8');
const matter = frontMatter(file);
// Stripping the markdown extension from the filename
const slug = filename.replace(path.extname(filename), '');
Expand All @@ -90,7 +112,10 @@ exports.run = function({ args, opts }) {
json: true,
...options,
})
.then(updateDoc.bind(null, slug, matter, hash), createDoc.bind(null, slug, matter, hash));
.then(updateDoc.bind(null, slug, matter, hash), createDoc.bind(null, slug, matter, hash))
.catch(err => {
return Promise.reject(err);
});
}),
);
};
29 changes: 22 additions & 7 deletions lib/login.js β†’ cmds/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@ const config = require('config');
const { validate: isEmail } = require('isemail');
const { promisify } = require('util');
const read = promisify(require('read'));

exports.desc = 'Login to a ReadMe project';
exports.category = 'services';
exports.weight = 1;

const configStore = require('../lib/configstore');

const testing = process.env.NODE_ENV === 'testing';

exports.command = 'login';
exports.usage = 'login';
exports.description = 'Login to a ReadMe project.';
exports.category = 'admin';
exports.weight = 1;

exports.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 {
Expand All @@ -24,7 +38,7 @@ async function getCredentials(opts) {
};
}

exports.run = async function({ opts }) {
exports.run = async function(opts) {
let { email, password, project, token } = opts;

// We only want to prompt for input outside of the test environment
Expand All @@ -34,7 +48,7 @@ exports.run = async function({ opts }) {
}

if (!project) {
return Promise.reject(new Error('No project subdomain provided. Please use --project'));
return Promise.reject(new Error('No project subdomain provided. Please use `--project`.'));
}

if (!isEmail(email)) {
Expand All @@ -57,6 +71,7 @@ exports.run = async function({ opts }) {
configStore.set('apiKey', res.apiKey);
configStore.set('email', email);
configStore.set('project', project);

return `Successfully logged in as ${email.green} in the ${project.blue} project`;
})
.catch(badRequest);
Expand Down
6 changes: 5 additions & 1 deletion lib/oas.js β†’ cmds/oas.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ const { promisify } = require('util');

const spawn = promisify(cp.spawn);

exports.desc = 'OAS related tasks. See https://www.npmjs.com/package/oas';
exports.command = 'oas';
exports.usage = 'oas';
exports.description = 'OAS related tasks. See https://npm.im/oas for more information.';
exports.category = 'utilities';
exports.weight = 4;

exports.args = [];

exports.run = function() {
return spawn(path.join(__dirname, '..', 'node_modules', '.bin', 'oas'), process.argv.slice(3), {
stdio: 'inherit',
Expand Down
24 changes: 24 additions & 0 deletions cmds/open.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const config = require('config');
const open = require('opn');
const configStore = require('../lib/configstore');

exports.command = 'open';
exports.usage = 'open';
exports.description = 'Open your current ReadMe project in the browser.';
exports.category = 'utilities';
exports.weight = 1;

exports.args = [];

exports.run = function(opts) {
const project = configStore.get('project');
if (!project) {
return Promise.reject(new Error(`Please login using \`${config.cli} ${exports.usage}\`.`));
}

return (opts.mockOpen || open)(config.hub.replace('{project}', project), {
wait: false,
}).then(() => {
console.log(`Opening ${config.hub.replace('{project}', project).green} in your browser...`);
});
};
Loading