├── src ├── __tests__ │ ├── __fixtures__ │ │ ├── blank │ │ │ └── .gitkeep │ │ └── project │ │ │ ├── template.js │ │ │ └── package.json │ ├── __snapshots__ │ │ └── cli.test.js.snap │ └── cli.test.js ├── util │ ├── debug.js │ ├── cache.js │ ├── get-day-path.js │ └── fs.js ├── templates │ └── day.js ├── lib │ ├── timing.js │ ├── left-pad.js │ ├── __tests__ │ │ ├── left-pad.test.js │ │ ├── timing.test.js │ │ ├── soft-require.js │ │ ├── advent-api.test.js │ │ └── assert-error.test.js │ ├── soft-require.js │ ├── assert-error.js │ └── advent-api.js ├── init.js ├── run.js └── cli.js ├── .npmignore ├── .eslintrc ├── .gitignore ├── bin └── advent.js ├── package.json └── README.md /src/__tests__/__fixtures__/blank/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | __mocks__ 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | coverage 4 | *.log 5 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/project/template.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.part1 = input => input 4 | exports.part2 = input => input 5 | -------------------------------------------------------------------------------- /src/util/debug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Our exported debug module created once to speed up startup time 5 | */ 6 | module.exports = require('debug')('advent') 7 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "adventConfig": { 3 | "year": "2016", 4 | "nameTemplate": "src/day{{num}}.js", 5 | "templateFile": "template.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/util/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ConfigStore = require('configstore') 4 | const pkg = require('../../package.json') 5 | 6 | const cache = new ConfigStore(pkg.name, {}) 7 | 8 | module.exports = cache 9 | -------------------------------------------------------------------------------- /src/templates/day.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Part 1 4 | // ====== 5 | 6 | const part1 = input => { 7 | return input 8 | } 9 | 10 | // Part 2 11 | // ====== 12 | 13 | const part2 = input => { 14 | return input 15 | } 16 | 17 | module.exports = { part1, part2 } 18 | -------------------------------------------------------------------------------- /src/lib/timing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @function timing 5 | */ 6 | const timing = (process = global.process) => { 7 | const start = process.hrtime() 8 | return () => { 9 | const [seconds, nanoseconds] = process.hrtime(start) 10 | return (seconds * 1e9 + nanoseconds) / 1e6 11 | } 12 | } 13 | 14 | module.exports = timing 15 | -------------------------------------------------------------------------------- /src/lib/left-pad.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @function leftPad 5 | * @param {string} string 6 | * @param {number} minLength 7 | * @param {string} char 8 | */ 9 | const leftPad = (string, minLength, char) => { 10 | const diff = Math.max(minLength - string.length, 0) 11 | return char.repeat(diff) + string 12 | } 13 | 14 | module.exports = leftPad 15 | -------------------------------------------------------------------------------- /src/util/get-day-path.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const leftPad = require('../lib/left-pad') 4 | 5 | const getDayPath = (day, template) => { 6 | // Create the day filename 7 | const stringDay = leftPad(String(day), 2, '0') // Make it always two length with leading 0 8 | return template.replace('{{num}}', stringDay) 9 | } 10 | 11 | module.exports = getDayPath 12 | -------------------------------------------------------------------------------- /bin/advent.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const cli = require('../src/cli') 5 | const debug = require('../src/util/debug') 6 | 7 | process.title = 'advent' 8 | 9 | cli() 10 | .then(output => { 11 | if (typeof output !== 'undefined') console.log(output) 12 | }) 13 | .catch(err => { 14 | debug('error running advent') 15 | console.error(err.stack) 16 | }) 17 | -------------------------------------------------------------------------------- /src/lib/__tests__/left-pad.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 'use strict' 3 | 4 | const leftPad = require('../left-pad') 5 | 6 | describe('left-pad', () => { 7 | test('repeats the given character', () => { 8 | expect(leftPad('', 8, '0')).toEqual('00000000') 9 | expect(leftPad('foo', 8, '0')).toEqual('00000foo') 10 | }) 11 | test('if base string is longer than minLength, no change', () => { 12 | expect(leftPad('foo', 2, '0')).toEqual('foo') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/lib/soft-require.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @function softRequire 5 | * @param {string} packagePath - The absolute path to the module you would like 6 | * to require 7 | * @param {Mixed} [defaultValue=null] - The value to return if the module cannot 8 | * be included 9 | */ 10 | const softRequire = (packagePath, defaultValue = null) => { 11 | try { 12 | return require(packagePath) 13 | } catch (ex) { 14 | return defaultValue 15 | } 16 | } 17 | 18 | module.exports = softRequire 19 | -------------------------------------------------------------------------------- /src/util/fs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Bluebird = require('bluebird') 4 | const fs = require('fs') 5 | const mkdirp = require('mkdirp') 6 | 7 | /** 8 | * Exported fs functions promisified. Done here so when multiple files need 9 | * promisified fs files that we only do it once to speed up startup time. 10 | */ 11 | exports.readFile = Bluebird.promisify(fs.readFile.bind(fs)) 12 | exports.writeFile = Bluebird.promisify(fs.writeFile.bind(fs)) 13 | exports.access = Bluebird.promisify(fs.access.bind(fs)) 14 | exports.mkdirp = mkdirp 15 | -------------------------------------------------------------------------------- /src/lib/assert-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const notContains = searchStr => fullStr => fullStr.indexOf(searchStr) === -1 4 | 5 | /** 6 | * @function assertError 7 | * @param {Mixed} value - The value to assert. If truthy, nothing happens 8 | * @param {Error} ErrorClass - The error class to use if the value if falsy 9 | * @param {Mixed} [args] - The arguments to pass to the error class 10 | */ 11 | const assertError = (value, ErrorClass = Error, ...args) => { 12 | if (value) return 13 | const error = new ErrorClass(...args) 14 | if (error.stack) { 15 | error.stack = error.stack 16 | .split('\n') 17 | .filter(notContains(__filename)) 18 | .join('\n') 19 | } 20 | throw error 21 | } 22 | 23 | module.exports = assertError 24 | -------------------------------------------------------------------------------- /src/lib/__tests__/timing.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 'use strict' 3 | 4 | const timing = require('../timing') 5 | 6 | describe('timing', () => { 7 | test('it keeps track of the time', () => { 8 | const process = { 9 | hrtime: jest.fn(() => [4, 10]) 10 | } 11 | const time = timing(process) 12 | expect(process.hrtime).toHaveBeenCalledTimes(1) 13 | process.hrtime.mockClear() 14 | process.hrtime.mockImplementation(() => [5, 11]) 15 | const end = time() 16 | expect(process.hrtime).toHaveBeenCalledWith([4, 10]) 17 | expect(end).toEqual(5000.000011) 18 | }) 19 | test('works with global process', () => { 20 | const time = timing() 21 | const end = time() 22 | expect(typeof end).toEqual('number') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/lib/__tests__/soft-require.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 'use strict' 3 | 4 | const path = require('path') 5 | const softRequire = require('../soft-require') 6 | 7 | describe('soft-require', () => { 8 | test('requires a module', () => { 9 | const filepath = path.resolve(__dirname, '..', 'soft-require') 10 | expect(softRequire(filepath)).toEqual(softRequire) 11 | }) 12 | test('uses the default value if the module does not exist', () => { 13 | const filepath = path.resolve(__dirname, 'does-not-exist') 14 | expect(softRequire(filepath)).toEqual(null) 15 | }) 16 | test('uses given default value if module does not exist', () => { 17 | const filepath = path.resolve(__dirname, 'does-not-exist') 18 | const defaultValue = { foo: 'bar' } 19 | expect(softRequire(filepath, defaultValue)).toEqual(defaultValue) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/lib/__tests__/advent-api.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 'use strict' 3 | 4 | const querystring = require('querystring') 5 | const nock = require('nock') 6 | const adventApi = jest.requireActual('../advent-api') 7 | 8 | describe('advent-api', () => { 9 | beforeAll(() => nock.cleanAll()) 10 | afterEach(() => nock.cleanAll()) 11 | 12 | test('Calls advent with appropriate headers', () => { 13 | expect.hasAssertions() 14 | const year = '2017' 15 | const day = '4' 16 | const session = 'session=mysession' 17 | nock(adventApi.ADVENT_HOST) 18 | .get(`/${year}/day/${day}/input`) 19 | .reply(function (uri) { 20 | return [200, querystring.stringify(this.req.headers)] 21 | }) 22 | // Real input will be a straight up input string, we're just testing the 23 | // headers here 24 | return adventApi.getInput({ year, day, session }).then(input => { 25 | const data = querystring.parse(input) 26 | expect(data).toHaveProperty('user-agent', adventApi.USER_AGENT) 27 | expect(data).toHaveProperty('cookie', session) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/lib/__tests__/assert-error.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 'use strict' 3 | 4 | const assertError = require('../assert-error') 5 | 6 | describe('assert-error', () => { 7 | test('does not throw error if value is truthy', () => { 8 | assertError(true) 9 | assertError('true') 10 | assertError(7403) 11 | }) 12 | test('throws error if falsy', () => { 13 | expect(() => assertError(false)).toThrow(Error) 14 | expect(() => assertError(0)).toThrow(Error) 15 | expect(() => assertError(null)).toThrow(Error) 16 | expect(() => assertError()).toThrow(Error) 17 | }) 18 | test('throws given error', () => { 19 | expect(() => assertError(false, ReferenceError)).toThrow(ReferenceError) 20 | expect(() => assertError(false, TypeError, 'My message')).toThrow(TypeError) 21 | expect(() => assertError(false, TypeError, 'My message')).toThrow( 22 | 'My message' 23 | ) 24 | class CustomError { 25 | constructor (message) { 26 | this.message = message 27 | } 28 | } 29 | expect(() => assertError(false, CustomError, 'foobar')).toThrow(CustomError) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | const debug = require('./util/debug') 6 | const { readFile, writeFile, access, mkdirp } = require('./util/fs') 7 | 8 | const init = config => { 9 | debug(`Checking for existing file: ${config.dayFilepath}`) 10 | return Promise.all([ 11 | readFile(config.templateFile, 'utf8'), 12 | access(config.dayFilepath, fs.constants.R_OK) 13 | .then(() => true) 14 | .catch({ code: 'ENOENT' }, () => false) 15 | ]).then(([template, existingFile]) => { 16 | if (existingFile) { 17 | debug('Found existing file') 18 | if (config.force) { 19 | debug('Forcing creating of file') 20 | } else { 21 | debug('Skipping creating of file') 22 | return 23 | } 24 | } 25 | debug('Generating %s from %s', config.dayFilepath, config.templateFile) 26 | const directory = path.dirname(config.dayFilepath) 27 | return mkdirp(directory) 28 | .then(() => writeFile(config.dayFilepath, template, 'utf8')) 29 | .then(() => { 30 | debug('Generated %s successfully', config.dayFilepath) 31 | }) 32 | }) 33 | } 34 | 35 | module.exports = init 36 | -------------------------------------------------------------------------------- /src/lib/advent-api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const request = require('request-promise') 4 | const assertError = require('./assert-error') 5 | const pkg = require('../../package.json') 6 | 7 | const ADVENT_HOST = 'http://adventofcode.com' 8 | const USER_AGENT = `node/${process.version} ${pkg.name}/${pkg.version}` 9 | 10 | /** 11 | * @function getInput 12 | * Gets the input from the advent of code website 13 | * @param {Object} options 14 | * @param {String|Number} options.year - The year to pull input from 15 | * @param {String|Number} options.day - The day to pull input from 16 | * @param {String} options.session - The advent session cookie to use 17 | */ 18 | const getInput = ({ year, day, session }) => { 19 | assertError(year, ReferenceError, 'You must provide a year') 20 | assertError(day, ReferenceError, 'You must provide a day') 21 | assertError(session, ReferenceError, 'You must provide your own session') 22 | const inputUri = `${ADVENT_HOST}/${year}/day/${day}/input` 23 | const headers = { 24 | 'User-Agent': USER_AGENT, 25 | Cookie: session 26 | } 27 | return request({ method: 'GET', uri: inputUri, headers }) 28 | } 29 | 30 | module.exports = { 31 | ADVENT_HOST, 32 | USER_AGENT, 33 | getInput 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advent-of-code", 3 | "version": "5.0.0", 4 | "description": "A cli to help initialize/run JavaScript advent-of-code challenges.", 5 | "main": "bin/advent.js", 6 | "scripts": { 7 | "lint": "standard && prettier-eslint '**/*.js' --list-different", 8 | "test": "jest", 9 | "format": "prettier-eslint '**/*.js' --write" 10 | }, 11 | "bin": { 12 | "advent": "./bin/advent.js" 13 | }, 14 | "keywords": [ 15 | "adventofcode", 16 | "advent", 17 | "puzzle" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/ksmithut/advent-of-code.git" 22 | }, 23 | "author": "ksmithut", 24 | "license": "MIT", 25 | "eslintConfig": { 26 | "root": true, 27 | "extends": [ 28 | "standard" 29 | ] 30 | }, 31 | "jest": { 32 | "collectCoverage": true, 33 | "testPathIgnorePatterns": [ 34 | "/node_modules/", 35 | "/__fixtures__/" 36 | ], 37 | "coverageThreshold": { 38 | "global": { 39 | "statements": 100, 40 | "branch": 100, 41 | "functions": 100, 42 | "lines": 100 43 | } 44 | } 45 | }, 46 | "dependencies": { 47 | "bluebird": "^3.7.2", 48 | "commander": "^6.0.0", 49 | "configstore": "^5.0.1", 50 | "debug": "^4.1.1", 51 | "get-stdin": "^8.0.0", 52 | "mkdirp": "^1.0.4", 53 | "request": "^2.88.2", 54 | "request-promise": "^4.2.5" 55 | }, 56 | "devDependencies": { 57 | "@types/jest": "^26.0.5", 58 | "jest": "^26.1.0", 59 | "nock": "^13.0.2", 60 | "prettier-eslint-cli": "^5.0.0", 61 | "rimraf": "^3.0.2", 62 | "standard": "^14.3.4" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/cli.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`advent cli init command inits file 1`] = ` 4 | Array [ 5 | Array [ 6 | ".gitkeep", 7 | "day05.js", 8 | ], 9 | "'use strict' 10 | 11 | // Part 1 12 | // ====== 13 | 14 | const part1 = input => { 15 | return input 16 | } 17 | 18 | // Part 2 19 | // ====== 20 | 21 | const part2 = input => { 22 | return input 23 | } 24 | 25 | module.exports = { part1, part2 } 26 | ", 27 | ] 28 | `; 29 | 30 | exports[`advent cli init command it uses --name-template argument 1`] = ` 31 | "'use strict' 32 | 33 | // Part 1 34 | // ====== 35 | 36 | const part1 = input => { 37 | return input 38 | } 39 | 40 | // Part 2 41 | // ====== 42 | 43 | const part2 = input => { 44 | return input 45 | } 46 | 47 | module.exports = { part1, part2 } 48 | " 49 | `; 50 | 51 | exports[`advent cli init command it uses package.json configuration 1`] = ` 52 | "'use strict' 53 | 54 | exports.part1 = input => input 55 | exports.part2 = input => input 56 | " 57 | `; 58 | 59 | exports[`advent cli init command keeps existing file in place 1`] = ` 60 | Array [ 61 | Array [ 62 | ".gitkeep", 63 | "day07.js", 64 | ], 65 | "'use strict'; console.log('hello')", 66 | ] 67 | `; 68 | 69 | exports[`advent cli init command overrides existing file if force command is sent 1`] = ` 70 | Array [ 71 | Array [ 72 | ".gitkeep", 73 | "day07.js", 74 | ], 75 | "'use strict' 76 | 77 | // Part 1 78 | // ====== 79 | 80 | const part1 = input => { 81 | return input 82 | } 83 | 84 | // Part 2 85 | // ====== 86 | 87 | const part2 = input => { 88 | return input 89 | } 90 | 91 | module.exports = { part1, part2 } 92 | ", 93 | ] 94 | `; 95 | -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getStdin = require('get-stdin') 4 | const timing = require('./lib/timing') 5 | const advent = require('./lib/advent-api') 6 | const cache = require('./util/cache') 7 | const debug = require('./util/debug') 8 | 9 | const getInput = (input, config) => { 10 | let inputPromise 11 | if (input === '-') { 12 | // If input is `-`, get from stdin 13 | debug('Input is stdin, reading stdin') 14 | inputPromise = getStdin() 15 | } else if (input === '+') { 16 | // If input is `+`, get from adventofcode 17 | const cacheKey = `${config.year}:${config.day}` 18 | debug(`Input is from adventofcode.com. Checking cache.`) 19 | const cachedValue = cache.get(cacheKey) 20 | if (cachedValue) { 21 | debug(`Cache found! Returning cached value`) 22 | inputPromise = Promise.resolve(cachedValue) 23 | } else { 24 | debug('Cache not found. Retrieving from adventofcode.com.') 25 | inputPromise = advent 26 | .getInput({ 27 | year: config.year, 28 | day: config.day, 29 | session: config.session 30 | }) 31 | .then(input => { 32 | cache.set(cacheKey, input) 33 | return input 34 | }) 35 | } 36 | } else { 37 | // Otherwise, just use raw input value 38 | debug('Getting raw input from argument') 39 | inputPromise = Promise.resolve(input) 40 | } 41 | return inputPromise.then(input => { 42 | debug('Successfully got input') 43 | return input 44 | }) 45 | } 46 | 47 | const run = (rawInput, config) => { 48 | let time 49 | return getInput(rawInput, config) 50 | .then(input => { 51 | debug(`Getting day module at ${config.dayFilepath}`) 52 | const dayModule = require(config.dayFilepath) 53 | const options = Object.assign( 54 | { 55 | noTrim: false 56 | }, 57 | dayModule.options 58 | ) 59 | if (!options.noTrim) input = input.trim() 60 | if (config.part === 1) { 61 | debug(`Running part 1`) 62 | time = timing() 63 | return dayModule.part1(input) 64 | } 65 | // istanbul ignore else - Input gets validated before this 66 | if (config.part === 2) { 67 | debug(`Running part 2`) 68 | time = timing() 69 | return dayModule.part2(input) 70 | } 71 | // istanbul ignore next - Input gets validated before this 72 | throw new RangeError('Not a valid part. Must be 1 or 2.') 73 | }) 74 | .then(output => { 75 | const end = time() 76 | debug(`Got output in ${end}ms`) 77 | return output 78 | }) 79 | } 80 | 81 | module.exports = run 82 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { Command } = require('commander') 5 | const softRequire = require('./lib/soft-require') 6 | const assertError = require('./lib/assert-error') 7 | const pkg = require('../package.json') 8 | const getDayPath = require('./util/get-day-path') 9 | const debug = require('./util/debug') 10 | 11 | const DECEMBER = 11 12 | const DEFAULT_TEMPLATE_FILE = path.resolve(__dirname, 'templates', 'day.js') 13 | 14 | const parseDay = day => { 15 | day = day.replace(/^[^\d]*/, '') 16 | day = Number.parseInt(day, 10) 17 | assertError( 18 | day >= 1 && day <= 25, 19 | RangeError, 20 | 'day must be between 1 and 25 inclusive' 21 | ) 22 | return day 23 | } 24 | 25 | const parsePart = part => { 26 | part = part.replace(/^[^\d]*/, '') 27 | part = Number.parseInt(part, 10) 28 | assertError(part === 1 || part === 2, RangeError, 'part must be 1 or 2') 29 | return part 30 | } 31 | 32 | const main = ( 33 | /* istanbul ignore next */ args = process.argv, 34 | program = new Command() 35 | ) => { 36 | const pkgPath = path.resolve('package.json') 37 | const userPkg = softRequire(pkgPath, { adventConfig: {} }).adventConfig || {} 38 | const now = new Date() 39 | const defaultYear = 40 | userPkg.year || 41 | (now.getMonth() === DECEMBER ? now.getFullYear() : now.getFullYear() - 1) 42 | const defaultNameTemplate = userPkg.nameTemplate || 'day{{num}}.js' 43 | const defaultTemplateFile = userPkg.templateFile 44 | ? path.resolve(userPkg.templateFile) 45 | : DEFAULT_TEMPLATE_FILE 46 | 47 | const getConfig = (day, args) => { 48 | const config = { 49 | year: args.year, 50 | session: args.session || process.env.ADVENT_SESSION, 51 | day: parseDay(day), 52 | nameTemplate: args.nameTemplate, 53 | templateFile: args.templateFile 54 | } 55 | const dayFilename = getDayPath(config.day, config.nameTemplate) 56 | config.dayFilepath = path.resolve(dayFilename) 57 | return config 58 | } 59 | 60 | program.version(pkg.version) 61 | 62 | program.on( 63 | '--help', 64 | /* istanbul ignore next */ 65 | () => 66 | console.log( 67 | [ 68 | '', 69 | ' Examples:', 70 | '', 71 | " advent run --day 1 --part 1 'this is my input'", 72 | ' cat input.txt | advent run -d 1 -p 1 -', 73 | ' advent run -d 1 -p 1 - < input.txt', 74 | " advent run -d 1 -p 1 + --session 'session=asefsafes...'", 75 | '', 76 | ' Notes:', 77 | '', 78 | ' For anything that reaches out to advent-of-code.com, you need to', 79 | ' provide your session token. You can get this by opening up the', 80 | ' network tab in the devtools, logging into to adventofcode.com, then', 81 | ' viewing what gets sent as the `Cookie:` request header on', 82 | ' subsequent requests. You may pass in the required value using', 83 | ' `--session [value]` or using the `ADVENT_SESSION` environment', 84 | ' variable. Note that it likely starts with `session=`', 85 | '' 86 | ].join('\n') 87 | ) 88 | ) 89 | 90 | let action 91 | /** 92 | * advent run 93 | */ 94 | program 95 | .command('run ') 96 | .description('Runs a given day with the given input') 97 | .option( 98 | '-y, --year [year]', 99 | 'Select the advent year you are running', 100 | defaultYear 101 | ) 102 | .option('-s, --session [cookie]', 'Session cookie to make requests') 103 | .option( 104 | '-n, --name-template [template]', 105 | 'The filename template to use when looking for day files', 106 | defaultNameTemplate 107 | ) 108 | .action((day, part, input, command) => { 109 | const run = require('./run') 110 | const config = getConfig(day, command) 111 | config.part = parsePart(part) 112 | debug('Running "run" with following config: %O', config) 113 | action = run(input, config) 114 | }) 115 | 116 | /** 117 | * advent init 118 | */ 119 | program 120 | .command('init ') 121 | .description('Initializes the file for a given day') 122 | .option( 123 | '-n, --name-template [template]', 124 | 'The filename template to use when looking for day files', 125 | defaultNameTemplate 126 | ) 127 | .option( 128 | '-t, --template-file [filepath]', 129 | 'The path to a template file', 130 | defaultTemplateFile 131 | ) 132 | .option('-f, --force', 'Will override an existing file', false) 133 | .action((day, command) => { 134 | const init = require('./init') 135 | const config = getConfig(day, command) 136 | config.force = command.force 137 | debug('Running "init" with following config: %O', config) 138 | action = init(config) 139 | }) 140 | 141 | program.parse(args) 142 | 143 | /* istanbul ignore next */ 144 | if (!action) return program.help() // NOTE this will terminate the program 145 | 146 | return action // This should be a promise of the action to take place 147 | } 148 | 149 | module.exports = main 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # advent-of-code 2 | 3 | A cli to help initialize/run JavaScript advent-of-code challenges. 4 | 5 | # Installation 6 | 7 | ```sh 8 | yarn add advent-of-code 9 | # or install globally 10 | yarn global add advent-of-code 11 | ``` 12 | 13 | # Configuration 14 | 15 | You can configure the `advent` cli using the command line arguments (documented 16 | below) or some of the arguments can be configured via a `package.json` file. 17 | 18 | Below are the available configuration options. If you pass in command-line 19 | arguments, they will override your `package.json` configuration. 20 | 21 | ```js 22 | { 23 | "adventConfig": { 24 | "year": "2016", 25 | "nameTemplate": "day{{num}}.js", 26 | "templateFile": "node_modules/advent-of-code/src/templates/day.js" 27 | } 28 | } 29 | ``` 30 | 31 | | `package.json` key | CLI argument | Default | Description | 32 | | --------------------------- | -------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 33 | | `adventConfig.year` | `-y, --year [year]` | currentMonth === December ? currentYear : currentYear - 1 | When pulling input from adventofcode.com, this year will be used. | 34 | | - | `-s, --session [cookie]` | `process.env.ADVENT_SESSION` | The session cookie to use when making requests to adventofcode.com. You can get this by logging into adventofcode.com and inspecting the request in your devtools and see what your cookie value is. Should start with `session=`. | 35 | | `adventConfig.nameTemplate` | `-n, --name-template [template]` | `'day{{num}}.js'` | The filename template to use when running and creating new day files. Wherever `{{num}}` is in the string, it will be replaced with a two digit (leading `0`s) representation of the number will be input. So if the day is `1`, using the default template, the filename will be `day01.js`. | 36 | | `adventConfig.templateFile` | `-t, --template-file [filepath]` | `'node_modules/advent-of-code/src/templates/day.js'` | The template file to use when initializing a new day file. It is recommended that you have your own that fits your style. The only requirement is that you export 2 functions: `exports.part1` and `exports.part2`, or just `module.exports = { part1, part2 }`. You may also export an `options` object to configure how input is parsed. `options.noTrim` lets you choose whether or not the input gets trimmed. Default is `false`. | 37 | | - | `-f, --force` | `false` | A flag used if you want to override an existing file with the template when calling `advent init` | 38 | 39 | # Usage 40 | 41 | ## Display help 42 | 43 | ```sh 44 | advent help 45 | ``` 46 | 47 | ## Initialize a day 48 | 49 | ```sh 50 | advent init 51 | ``` 52 | 53 | ### Options 54 | 55 | * `` - The day to initialize. Will create a file using your `nameTemplate` 56 | configuration. You can run `advent init ` again and it won't do anything 57 | unless you pass the `--force` flag. 58 | * `--name-template [template]` - See configuration above 59 | * `--template-file [filepath]` - See configuration above 60 | * `--force` - See configuration above 61 | 62 | ## Run a day's code 63 | 64 | ```sh 65 | $ advent run 66 | ``` 67 | 68 | ### Options 69 | 70 | * `` - The day to initialize. Will use the file in the configuration you 71 | set for `nameTemplate` 72 | * `` - The part to run. The day file should export a property called 73 | `part1` and `part2`. 74 | * `` - The input to give the function. If `-` is passed, stdin will be 75 | used as the input. If `+` is passed, and you have a session set, then it will 76 | pull the input from adventofcode.com, or the cached value once it pulls from 77 | adventofcode.com the first time. 78 | * `--year [year]` - See configuration above 79 | * `--session [session]` - See configuration above 80 | * `--name-template [template]` - See configuration above 81 | 82 | # Notes 83 | 84 | * This module leverages the [debug](https://www.npmjs.com/package/debug) module. 85 | Setting `DEBUG=advent` will print out debug information, such as when this 86 | module is pulling from local cache, which days it's trying to run/initialize, 87 | and so forth. When reporting bugs, please have the output from this handy so 88 | that I can more quickly determine the issue. 89 | 90 | * One thing I liked to do with my local stuff was to store my answers locally 91 | along with example inputs (from the descriptions). The goal for this project 92 | was to make it easy for someone to upload their solutions to github, and 93 | others could pull it down and have it work with their inputs, but if there is 94 | interest in providing a "test suite" to test against example inputs and such, 95 | then I will do so. 96 | 97 | # Disclaimer 98 | 99 | I am not affiliated with [adventofcode.com](http://adventofcode.com) or any of 100 | their sponsors, employees, pets, or anything relating to them. I am an active 101 | participant, and I wanted to make a tool to make it easier to setup and run 102 | advent of code things. Please don't abuse adventofcode.com. This tool could be 103 | used to make a lot of automated requests to their site, which is why this tool 104 | leverages caching. If you find that you're making too many requests to 105 | adventofcode.com because of this module, please let me know so I can resolve any 106 | issues. If this module is used to abuse adventofcode.com, I will unpublish it 107 | from npm and remove this code from github. 108 | -------------------------------------------------------------------------------- /src/__tests__/cli.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 'use strict' 3 | 4 | // mocks 5 | jest 6 | .mock('get-stdin', () => { 7 | let input = '' 8 | const getInput = () => Promise.resolve(input) 9 | getInput.__setInput = val => { 10 | input = val 11 | } 12 | getInput.__resetInput = getInput.__setInput.bind(null, '') 13 | return getInput 14 | }) 15 | .mock('../util/cache', () => { 16 | let data = {} 17 | const cache = { 18 | get: key => data[key], 19 | set: (key, value) => { 20 | data[key] = value 21 | return value 22 | }, 23 | clear: () => { 24 | data = {} 25 | } 26 | } 27 | return cache 28 | }) 29 | .mock('../lib/advent-api', () => { 30 | let input = '' 31 | const getInput = jest.fn(() => Promise.resolve(input)) 32 | const __setInput = val => { 33 | input = val 34 | } 35 | const __resetInput = __setInput.bind(null, '') 36 | return { 37 | getInput, 38 | __setInput, 39 | __resetInput 40 | } 41 | }) 42 | 43 | const fs = require('fs') 44 | const path = require('path') 45 | const Bluebird = require('bluebird') 46 | const rimraf = require('rimraf') 47 | const getStdin = require('get-stdin') // mocked above 48 | const cache = require('../util/cache') // mocked above 49 | const adventApi = require('../lib/advent-api') // mocked above 50 | const cli = require('../cli') 51 | 52 | Bluebird.promisifyAll(fs) 53 | const rimrafAsync = Bluebird.promisify(rimraf) 54 | 55 | const fixtures = path.resolve.bind(path, __dirname, '__fixtures__') 56 | const createArgs = (...args) => [process.argv[0], __filename].concat(args) 57 | 58 | describe('advent cli', () => { 59 | const cwd = process.cwd() 60 | afterEach(() => process.chdir(cwd)) 61 | afterEach(() => 62 | Promise.all([ 63 | rimrafAsync(fixtures('blank', '*.js')), 64 | rimrafAsync(fixtures('blank', 'my')), 65 | rimrafAsync(fixtures('project', 'src')) 66 | // rimrafAsync(fixtures('project', 'my')) 67 | ]) 68 | ) 69 | afterEach(() => getStdin.__resetInput()) 70 | afterEach(() => adventApi.__resetInput()) 71 | afterEach(() => cache.clear()) 72 | 73 | describe('init command', () => { 74 | test('inits file', () => { 75 | process.chdir(fixtures('blank')) 76 | return cli(createArgs('init', '5')) 77 | .then(() => 78 | Promise.all([ 79 | fs.readdirAsync(fixtures('blank')), 80 | fs.readFileAsync(fixtures('blank', 'day05.js'), 'utf8') 81 | ]) 82 | ) 83 | .then(results => { 84 | expect(results).toMatchSnapshot() 85 | }) 86 | }) 87 | 88 | test('keeps existing file in place', () => { 89 | const myfile = `'use strict'; console.log('hello')` 90 | process.chdir(fixtures('blank')) 91 | return cli(createArgs('init', '7')) 92 | .then(() => fs.writeFileAsync(fixtures('blank', 'day07.js'), myfile)) 93 | .then(() => cli(createArgs('init', '7'))) 94 | .then(() => 95 | Promise.all([ 96 | fs.readdirAsync(fixtures('blank')), 97 | fs.readFileAsync(fixtures('blank', 'day07.js'), 'utf8') 98 | ]) 99 | ) 100 | .then(results => { 101 | expect(results).toMatchSnapshot() 102 | }) 103 | }) 104 | 105 | test('overrides existing file if force command is sent', () => { 106 | const myfile = `'use strict'; console.log('hello')` 107 | process.chdir(fixtures('blank')) 108 | return cli(createArgs('init', '7')) 109 | .then(() => fs.writeFileAsync(fixtures('blank', 'day07.js'), myfile)) 110 | .then(() => cli(createArgs('init', '7', '--force'))) 111 | .then(() => 112 | Promise.all([ 113 | fs.readdirAsync(fixtures('blank')), 114 | fs.readFileAsync(fixtures('blank', 'day07.js'), 'utf8') 115 | ]) 116 | ) 117 | .then(results => { 118 | expect(results).toMatchSnapshot() 119 | }) 120 | }) 121 | 122 | test('it uses --name-template argument', () => { 123 | process.chdir(fixtures('blank')) 124 | return cli( 125 | createArgs( 126 | 'init', 127 | '21', 128 | '--name-template', 129 | 'my/deep/{{num}}/program.js' 130 | ) 131 | ) 132 | .then(() => 133 | fs.readFileAsync( 134 | fixtures('blank', 'my', 'deep', '21', 'program.js'), 135 | 'utf8' 136 | ) 137 | ) 138 | .then(file => { 139 | expect(file).toMatchSnapshot() 140 | }) 141 | }) 142 | 143 | test('it uses package.json configuration', () => { 144 | process.chdir(fixtures('project')) 145 | return cli(createArgs('init', '25')) 146 | .then(() => 147 | fs.readFileAsync(fixtures('project', 'src', 'day25.js'), 'utf8') 148 | ) 149 | .then(file => { 150 | expect(file).toMatchSnapshot() 151 | }) 152 | }) 153 | }) 154 | 155 | describe('run command', () => { 156 | test('runs the given part', () => { 157 | process.chdir(fixtures('project')) 158 | const file = ` 159 | exports.part1 = (input) => input + '--part1' 160 | exports.part2 = (input) => input + '--part2' 161 | ` 162 | return cli(createArgs('init', '1')) 163 | .then(() => 164 | fs.writeFileAsync( 165 | fixtures('project', 'src', 'day01.js'), 166 | file, 167 | 'utf8' 168 | ) 169 | ) 170 | .then(() => cli(createArgs('run', '1', '1', 'myinput'))) 171 | .then(output => { 172 | expect(output).toEqual('myinput--part1') 173 | }) 174 | .then(() => cli(createArgs('run', '1', '2', 'myinput'))) 175 | .then(output => { 176 | expect(output).toEqual('myinput--part2') 177 | }) 178 | }) 179 | }) 180 | 181 | test('gets input from stdin', () => { 182 | process.chdir(fixtures('project')) 183 | getStdin.__setInput('myinput') 184 | const file = ` 185 | exports.part1 = (input) => input + '--part1' 186 | exports.part2 = (input) => input + '--part2' 187 | ` 188 | return cli(createArgs('init', '1')) 189 | .then(() => 190 | fs.writeFileAsync(fixtures('project', 'src', 'day01.js'), file, 'utf8') 191 | ) 192 | .then(() => cli(createArgs('run', '1', '1', '-'))) 193 | .then(output => { 194 | expect(output).toEqual('myinput--part1') 195 | }) 196 | .then(() => cli(createArgs('run', '1', '2', '-'))) 197 | .then(output => { 198 | expect(output).toEqual('myinput--part2') 199 | }) 200 | }) 201 | 202 | test('gets input from adventofcode, and caches it', () => { 203 | process.chdir(fixtures('project')) 204 | adventApi.__setInput('myinput') 205 | const file = ` 206 | exports.part1 = (input) => input + '--part1' 207 | exports.part2 = (input) => input + '--part2' 208 | ` 209 | return cli(createArgs('init', '1')) 210 | .then(() => 211 | fs.writeFileAsync(fixtures('project', 'src', 'day01.js'), file, 'utf8') 212 | ) 213 | .then(() => cli(createArgs('run', '1', '1', '+', '--session', 'foobar'))) 214 | .then(output => { 215 | expect(output).toEqual('myinput--part1') 216 | expect(adventApi.getInput).toHaveBeenCalledTimes(1) 217 | }) 218 | .then(() => cli(createArgs('run', '1', '2', '+', '--session', 'foobar'))) 219 | .then(output => { 220 | expect(output).toEqual('myinput--part2') 221 | expect(adventApi.getInput).toHaveBeenCalledTimes(1) 222 | }) 223 | }) 224 | 225 | test('it does not trim when option is set', () => { 226 | cache.clear() 227 | process.chdir(fixtures('project')) 228 | adventApi.__setInput(' myinput ') 229 | const file = ` 230 | exports.part1 = (input) => input + '--part1foobar' 231 | exports.part2 = (input) => input + '--part2foobar' 232 | exports.options = { noTrim: true } 233 | ` 234 | return cli(createArgs('init', '1')) 235 | .then(() => 236 | fs.writeFileAsync(fixtures('project', 'src', 'day02.js'), file, 'utf8') 237 | ) 238 | .then(() => cli(createArgs('run', '2', '1', '+', '--session', 'foobar'))) 239 | .then(output => { 240 | expect(output).toEqual(' myinput --part1foobar') 241 | }) 242 | .then(() => cli(createArgs('run', '2', '2', '+', '--session', 'foobar'))) 243 | .then(output => { 244 | expect(output).toEqual(' myinput --part2foobar') 245 | }) 246 | }) 247 | }) 248 | --------------------------------------------------------------------------------