dailyprogrammer
Version:
The /r/dailyprogrammer Tool provides you with a cli to quickly initialize programming challenges and publish them.
162 lines (147 loc) • 6.73 kB
text/typescript
import {ICommand} from "./ICommand";
import {existsSync, writeFile, readdir} from "fs";
import {exec} from "child_process";
import {IChallengeProvider} from "../challenges/IChallengeProvider";
import {Difficulty} from "../challenges/Difficulty";
import {IChallenge} from "../challenges/IChallenge";
import {OptParserUtil} from "./OptParserUtil";
import {resolve} from "path";
import {EOL} from "os";
import readline = require('readline');
import commander = require('commander');
export class InitCommand implements ICommand {
constructor(protected challengeProvider: IChallengeProvider,
protected stdout: NodeJS.WritableStream = process.stdout,
protected stdin: NodeJS.ReadableStream = process.stdin,
protected stderr: NodeJS.WritableStream = process.stderr) {
}
/**
* reddit-dp init [options] <challenge_no> <target_dir>
* Initializes target_dir with a specific language and template
*
* @param program
* @returns {ICommand}
*/
register(program: commander.ICommand): commander.ICommand {
return program.command('init')
.description('initialize a challenge identified by the challenge_no (e.g. 123) in target_dir')
.option('-l, --language [language]', 'the language you want to program with [javascript]', 'javascript')
.arguments('<challenge_no>')
.arguments('<target_dir>')
.action((challenge_no: string, target_dir: string, options: { language: string }) => {
let id = parseInt(challenge_no);
if (isNaN(id)) {
throw new Error('invalid challenge_no');
}
let cacheDir = resolve(process.env.HOME, '.reddit-dp/cache/');
let templateDir = resolve(cacheDir, options.language);
let targetDir = resolve(target_dir);
let repo = 'https://github.com/korve/dailyprogrammer-templates.git';
this.ensureDirEmpty(targetDir)
.then(() => this.challengeProvider.get(id))
.then(challenges => {
if (challenges.length === 0) {
throw new Error(`challenge #${id} was not found`);
}
if (challenges.length > 1) {
return this.promptDifficulty(challenges);
}
return challenges[0];
})
.then((challenge: IChallenge) => {
this.stdout.write(`Initializing challenge #${id} for language ${options.language}...${EOL}`);
return new Promise((resolve, reject) => {
let command = `mkdir -p ${cacheDir} && mkdir -p ${targetDir} && cd ${cacheDir} && git pull ${repo}`;
exec(command, (err: Error, stdout: string, stderr: string) => {
if (err) {
return reject(err);
}
this.stdout.write(stdout);
resolve(challenge);
});
});
})
.then((challenge: IChallenge) => this.cloneTemplateDir(templateDir, targetDir, challenge))
.then((challenge: IChallenge) => this.generateReadme(challenge, targetDir))
.catch(err => {
this.stderr.write(err.message + EOL);
process.exit(1);
})
.then(() => {
this.stdout.write(`Challenge #${id} with ${options.language} initialized at ${templateDir}${EOL}`);
this.stdout.write(`Use 'reddit-dp publish' to post your solution to /r/dailyprogramming`);
process.exit();
});
});
}
/**
* Clones template directory into project directory.
* @param templateDir
* @param targetDir
* @param challenge
* @returns {Promise<T>|Promise}
*/
private cloneTemplateDir(templateDir: string, targetDir: string, challenge: IChallenge): Promise<IChallenge> {
if (!existsSync(templateDir)) {
throw new Error('Error while initializing challenge: no template for language \'javascript\' found');
}
return new Promise((resolve, reject) => {
exec(`cp -R ${templateDir}/ ${targetDir}`, ( err => {
if (err) {
return reject(err);
}
resolve(challenge);
} ));
});
}
generateReadme(challenge: IChallenge, targetDir: string): Promise<IChallenge> {
return new Promise((resolve, reject) => {
if (!challenge.description) {
resolve(challenge);
}
writeFile(`${targetDir}/README.md`, challenge.description, (err) => {
if (err) {
reject(`Error while initializing template: could not write to ${targetDir}${EOL}`);
}
resolve(challenge);
});
});
};
/**
* Prompts the user to choose a difficulty
* @param challenges
* @returns {Promise<T>|Promise}
*/
private promptDifficulty(challenges: IChallenge[]): Promise<IChallenge> {
return new Promise((resolve) => {
let choices = challenges.map(c => `${c.difficulty}=${Difficulty[c.difficulty]}`);
let rl = readline.createInterface({
input: this.stdin,
output: this.stdout
});
rl.question(`Multiple challenges found for #${challenges[0].id}. Choose difficulty [${choices.join(', ')}]: `, (answer) => {
let difficulty = OptParserUtil.parseDifficulty(answer);
resolve(challenges.filter(c => c.difficulty === difficulty)[0]);
rl.close();
});
});
}
/**
* Checks if targetDir is empty and rejects the promise if
* targetDir is not empty.
* @param targetDir
* @returns {Promise<void>|Promise}
*/
private ensureDirEmpty(targetDir: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
readdir(targetDir, (err: Error, files: string[]) => {
if (err || files.length === 0) {
resolve();
} else {
reject(new Error(`${targetDir} is not empty`));
}
;
});
})
}
}