├── 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 |
--------------------------------------------------------------------------------