In order to learn more about Node.js and promises I followed a tutorial on Sitepoint about building a command line app. I liked the idea and simplicity of Node so I decided to build my own that initializes a local git repo and using the Bitbucket 2.0 API creates a remote repo. Now that I have it working I would love some feedback on how to make the code cleaner or more closely follow best practices. I would love any feedback people have. I have published it on NPM for people to use but I would love to make it better. Source code is here. Does anyone have any suggestions?
/**
* Prompt the user for Key & Secret
* @param callback - Uses the key and secret to gain access tokens
*/
function getCredentials(callback) {
let questions = [
{
name: 'key',
type: 'key',
message: 'Enter your OAuth Consumer Key:',
validate: function (value) {
if (value.length) {
return true;
} else {
return 'Please enter your consumer key';
}
}
},
{
name: 'secret',
type: 'secret',
message: 'Enter your OAuth Consumer Secret:',
validate: function (value) {
if (value.length) {
return true;
} else {
return 'Please enter your consumer secret';
}
}
}
];
inquirer.prompt(questions).then(callback);
}
/**
* Check if there is a local token in prefs
* @type {Promise} Returns the token in memory if found
*/
function getSavedToken() {
return new Promise((resolve, reject) => {
// If token exists in prefs, return it...
if (prefs.schemeinit && prefs.schemeinit.token != '') {
console.log(chalk.green('\n' + 'Token already in memory.' + '\n'));
resolve(prefs.schemeinit.token);
}
// Otherwise reject
reject(new Error('No token saved in prefs.'));
})
}
/**
* Get a new token
* @returns {Promise} Returns a new access token
*/
function getNewToken() {
return new Promise((resolve, reject) => {
getCredentials(function (credentials) {
let status = new Spinner('Authenticating you, please wait...');
status.start();
// Using Api endpoints
request({
url: 'https://bitbucket.org/site/oauth2/access_token',
method: 'POST',
headers: {'content-type': 'application/json'},
auth: {
user: credentials.key,
pass: credentials.secret
},
form: {
'grant_type': 'client_credentials'
}
}, function (err, res) {
status.stop();
if (err) {
reject(new Error('Could not get new token.'));
}
if (res.statusCode == 200) {
let json = JSON.parse(res.body);
// Set the token in prefs, also set key and secret as it was successfull
prefs.schemeinit = {
token: json.access_token,
refresh: json.refresh_token,
key: credentials.key,
secret: credentials.secret
};
resolve(json.access_token);
}
});
});
});
}
/**
* Refresh the access token
* @returns {Promise} Sets new token info in prefs and returns the new access token
*/
function refreshToken() {
console.log(chalk.green('\n' + 'Refreshing token...' + '\n'));
return new Promise((resolve, reject) => {
let status = new Spinner('Token expired, getting a new one...');
status.start();
request({
url: 'https://bitbucket.org/site/oauth2/access_token',
method: 'POST',
headers: {'content-type': 'application/json'},
auth: {
user: prefs.schemeinit.key,
pass: prefs.schemeinit.secret
},
form: {
'grant_type': 'refresh_token',
'refresh_token': prefs.schemeinit.refresh
}
}, function (err, res) {
status.stop();
if (err) {
console.log(chalk.red(err.message + '\n'));
reject(new Error('Couldn\'t get refresh token.'));
}
if (res.statusCode == 400) {
let json = JSON.parse(res.body);
console.log(chalk.red(res.statusMessage));
console.log(chalk.red(json.error_description + '\n'));
}
if (res.statusCode == 200) {
let json = JSON.parse(res.body);
// TODO: Do I need to re-set the key and secret here? If I don't does it wipe them?
// Update the token in prefs
prefs.schemeinit = {
token: json.access_token,
refresh: json.refresh_token,
key: prefs.schemeinit.key,
secret: prefs.schemeinit.secret
};
console.log(chalk.yellow('New Tokens received.' + '\n'));
resolve(json.access_token);
}
});
})
}
/**
* Get User
* @param {string} token - OAuth Token
* @returns {Promise} Return the username
*/
function getUser(token) {
return new Promise((resolve, reject) => {
request({
url: 'https://api.bitbucket.org/2.0/user',
method: 'GET',
headers: {'Authorization': 'Bearer ' + token}
}, function (err, res) {
let user = JSON.parse(res.body);
if (err) {
reject(new Error(err));
}
if (res.statusCode == 401) {
reject(new Error('Token expired. Need to refresh token.'));
}
if (res) {
resolve(user.username);
}
});
});
}
/**
* Create the remote repository on Bitbucket
* @param {string} username - Bitbucket username
* @returns {Promise} Returns object of newly created repo information
*/
function createRepo(username) {
return new Promise((resolve, reject) => {
let questions = [
{
type: 'input',
name: 'reponame',
message: 'Enter a name for the repository:',
default: files.getCurrentDirectoryBase(),
validate: function (value) {
if (value.length) {
return true;
} else {
return 'Please enter a name for the repository';
}
}
},
{
type: 'input',
name: 'description',
default: null,
message: 'Optionally enter a description of the repository:'
},
{
type: 'list',
name: 'visibility',
message: 'Public or private:',
choices: ['public', 'private'],
default: 'private'
},
{
type: 'list',
name: 'language',
message: 'Language:',
choices: ['css', 'html/css', 'javascript', 'nodejs', 'php'],
default: 'php'
}
];
inquirer.prompt(questions).then(function (answers) {
let status = new Spinner('Creating repository...');
status.start();
request({
url: 'https://api.bitbucket.org/2.0/repositories/' + username + '/' + _.kebabCase(answers.reponame),
method: 'POST',
headers: {'Authorization': 'Bearer ' + prefs.schemeinit.token},
form: {
"scm": "git",
"name": answers.reponame,
"is_private": answers.visibility === 'private' ? true : false,
"description": answers.description,
"language": answers.language,
}
}, function (err, res) {
status.stop();
if (err) {
console.log(err);
reject(new Error('Couldn\'t create remote repo.'));
}
let json = JSON.parse(res.body);
if(res.statusCode == 400) {
reject(new Error(json.error.message));
}
if (res.statusCode == 200) {
console.log(chalk.green('\n' + json.name + ' created sucessfully.'));
console.log(chalk.green('You can view it here: ' + json.links.html.href + '\n'));
resolve(json);
}
});
});
});
}
/**
* Create a local .gitignore file
* @returns {Promise}
*/
function createGitignore() {
return new Promise((resolve, reject) => {
let filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
if (filelist.length) {
inquirer.prompt(
[
{
type: 'checkbox',
name: 'ignore',
message: 'Select the local files and/or folders you wish to ignore:',
choices: filelist,
default: ['node_modules', 'bower_components']
}
]
).then(function (answers) {
if (answers.ignore.length) {
fs.writeFileSync('.gitignore', answers.ignore.join('\n'));
} else {
touch('.gitignore');
}
resolve();
});
} else {
touch('.gitignore');
resolve();
}
})
}
/**
* Create a local git repository and set it's remote to
* previously created remote repository
* @param {object} remoteRepo - Information object for remote repo
* @returns {Promise}
*/
function localRepo(remoteRepo) {
return new Promise((resolve, reject) => {
let status = new Spinner('Setting up the repository...');
status.start();
git
.init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', 'https://bitbucket.org/' + remoteRepo.full_name)
.push('origin', 'master')
.exec(function () {
status.stop();
resolve('Local repository created and connected to remote.');
});
})
}
/**
* Start the process by creating the remote repository and returning it
* @type {Promise.<TResult>}
*/
const repo = getSavedToken()
.catch(err => {
// if no local token, get a new one.
console.log(chalk.yellow('\n' + err.message + '\n'));
return getNewToken();
})
.then(getUser)
.catch(err => {
// if error, most likely that the token needs to be refreshed
console.log(chalk.yellow(err.message));
return refreshToken()
.then(getUser);
})
.then(createRepo);
/**
* Once remote repository is created, setup the local repository
* and connect to the remote
*/
repo.then(res => {
let remoteRepo = res;
createGitignore()
.then(res => {
console.log(chalk.green('\n' + '.gitignore created.'));
return localRepo(remoteRepo);
})
.then(res => {
console.log(chalk.green('\n' + res));
console.log(chalk.green('All done! Now go take over the world!' + '\n'));
});
})
.catch(err => {
console.log(chalk.red('\n' + err.message + '\n'));
});