├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .env.example ├── .eslintrc ├── .gitignore ├── .node-version ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── github-app-repl.js ├── lib ├── app.js ├── installation.js ├── private-key.js └── utils.js ├── package-lock.json ├── package.json └── script └── historytailnode /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM node:12 7 | 8 | # The node image includes a non-root user with sudo access. Use the "remoteUser" 9 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 10 | # will be updated to match your local UID/GID (when using the dockerFile property). 11 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 12 | ARG USERNAME=node 13 | ARG USER_UID=1000 14 | ARG USER_GID=$USER_UID 15 | 16 | # Set to false to skip installing zsh and Oh My ZSH! 17 | ARG INSTALL_ZSH="true" 18 | 19 | # Location and expected SHA for common setup script - SHA generated on release 20 | ARG COMMON_SCRIPT_SOURCE="https://raw.githubusercontent.com/microsoft/vscode-dev-containers/master/script-library/common-debian.sh" 21 | ARG COMMON_SCRIPT_SHA="dev-mode" 22 | 23 | # Avoid warnings by switching to noninteractive 24 | ENV DEBIAN_FRONTEND=noninteractive 25 | 26 | ENV PATH=${PATH}:/usr/local/share/npm-global/bin 27 | 28 | # Configure apt and install packages 29 | RUN apt-get update \ 30 | && apt-get -y install --no-install-recommends apt-utils dialog wget ca-certificates 2>&1 \ 31 | # 32 | # Verify git, common tools / libs installed, add/modify non-root user, optionally install zsh 33 | && wget -q -O /tmp/common-setup.sh $COMMON_SCRIPT_SOURCE \ 34 | && if [ "$COMMON_SCRIPT_SHA" != "dev-mode" ]; then echo "$COMMON_SCRIPT_SHA /tmp/common-setup.sh" | sha256sum -c - ; fi \ 35 | && /bin/bash /tmp/common-setup.sh "$INSTALL_ZSH" "$USERNAME" "$USER_UID" "$USER_GID" \ 36 | && rm /tmp/common-setup.sh \ 37 | # 38 | # Remove outdated yarn from /opt and install via package 39 | # so it can be easily updated via apt-get upgrade yarn 40 | && rm -rf /opt/yarn-* \ 41 | && rm -f /usr/local/bin/yarn \ 42 | && rm -f /usr/local/bin/yarnpkg \ 43 | && apt-get install -y curl apt-transport-https lsb-release \ 44 | && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ 45 | && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 46 | && apt-get update \ 47 | && apt-get -y install --no-install-recommends yarn \ 48 | # 49 | # Set alternate global install location that both users have rights to access 50 | && mkdir -p /usr/local/share/npm-global \ 51 | && chown ${USERNAME}:root /usr/local/share/npm-global \ 52 | && npm config -g set prefix /usr/local/share/npm-global \ 53 | && sudo -u ${USERNAME} npm config -g set prefix /usr/local/share/npm-global \ 54 | # 55 | # Tactically remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 56 | # Can leave in Dockerfile once upstream base image moves to > 7.0.7-28. 57 | && apt-get purge -y imagemagick imagemagick-6-common \ 58 | # 59 | # Install jq 60 | && apt-get install -y jq \ 61 | # 62 | # Install eslint globally 63 | && sudo -u ${USERNAME} npm install -g eslint \ 64 | # 65 | # Clean up 66 | && apt-get autoremove -y \ 67 | && apt-get clean -y \ 68 | && rm -rf /var/lib/apt/lists/* 69 | 70 | # Switch back to dialog for any ad-hoc use of apt-get 71 | ENV DEBIAN_FRONTEND=dialog 72 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js 12", 3 | "dockerFile": "Dockerfile", 4 | 5 | // Set *default* container specific settings.json values on container create. 6 | "settings": { 7 | "terminal.integrated.shell.linux": "/bin/bash" 8 | }, 9 | 10 | // Add the IDs of extensions you want installed when the container is created. 11 | "extensions": [ 12 | "dbaeumer.vscode-eslint" 13 | ], 14 | 15 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 16 | // "forwardPorts": [], 17 | 18 | // Use 'postCreateCommand' to run commands after the container is created. 19 | "postCreateCommand": "npm install", 20 | 21 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 22 | // "remoteUser": "node" 23 | } 24 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # The ID of your GitHub App 2 | APP_ID= 3 | 4 | # The path to a private key for your GitHub App 5 | PRIVATE_KEY_PATH= 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "prettier" 5 | ], 6 | "env": { 7 | "es6": true, 8 | "node": true, 9 | "jest": true 10 | }, 11 | "rules": { 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "trailingComma": "none", 16 | "singleQuote": true, 17 | "printWidth": 120 18 | } 19 | ] 20 | }, 21 | "plugins": [ 22 | "prettier" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Private key for GitHub Apps 64 | *.pem 65 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.10.0 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at {{ email }}. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: /fork 4 | [pr]: /compare 5 | [code-of-conduct]: CODE_OF_CONDUCT.md 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 10 | 11 | ## Issues and PRs 12 | 13 | If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them. 14 | 15 | We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR. 16 | 17 | ## Submitting a pull request 18 | 19 | 1. [Fork][fork] and clone the repository. 20 | 1. Create a new branch: `git checkout -b my-branch-name`. 21 | 1. Make your change, add tests, and make sure the tests still pass. 22 | 1. Push to your fork and [submit a pull request][pr]. 23 | 1. Pat your self on the back and wait for your pull request to be reviewed and merged. 24 | 25 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 26 | 27 | - Write and update tests. 28 | - Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 29 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 30 | 31 | Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you. 32 | 33 | ## Resources 34 | 35 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 36 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 37 | - [GitHub Help](https://help.github.com) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Steve Winton (https://github.com/swinton) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub-App-REPL 2 | 3 | > 🔁 A REPL (read–eval–print loop) for GitHub Apps 4 | 5 | ## Features 6 | 7 | 1. Uses [the Node `repl` module](https://nodejs.org/api/repl.html) :repeat: 8 | 1. With [experimental `await` support](https://nodejs.org/api/cli.html#cli_experimental_repl_await) enabled :ok_hand: 9 | 1. Loads your GitHub App and exposes it as `app` in a REPL session :zap: 10 | 1. So you can easily start playing around with it interactively :relaxed: 11 | 12 | ## Why? 13 | 14 | I like using a REPL to experiment with ideas and get comfortable with an API before delving into actual code. This removes some of the boilerplate code required to start working with GitHub Apps in a REPL. 15 | 16 | ## Installation 17 | 18 | ``` 19 | npm install -D github-app-repl 20 | ``` 21 | 22 | ## Configuration 23 | 24 | A couple of environment variables are required for configuration: 25 | 26 | 1. **`APP_ID`**: This should be set to the ID of your GitHub App. 27 | - This is available from the settings page of your GitHub App, as `App ID`. 28 | 1. **`PRIVATE_KEY_PATH`**: This should be set to a local filesystem path to a private key for your GitHub App. 29 | - More information on generating and downloading private keys for your GitHub Apps is [available in the documentation](https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#generating-a-private-key). 30 | 31 | These environment variables can either be set directly, or via a `.env` file that follows [this example](https://github.com/swinton/github-app-repl/blob/master/.env.example). 32 | 33 | ## Usage 34 | 35 | ``` 36 | npx github-app-repl 37 | ``` 38 | 39 | ## API 40 | 41 | - `app.id`: GitHub App ID 42 | - `app.name`: GitHub App Name 43 | - `app.description`: GitHub App Description 44 | - `app.octokit`: Authenticated [`@octokit/rest`](https://github.com/octokit/rest.js) instance, scoped to your GitHub App 45 | - `app.installations()`: Dictionary of GitHub App Installations, keyed by installation account login 46 | - `installations`: Pre-loaded dictionary of GitHub App Installations, keyed by installation account login 47 | - `installations[:login].octokit`: Authenticated [`@octokit/rest`](https://github.com/octokit/rest.js) instance, scoped to a specific GitHub App installation 48 | - `installations[:login].graphql`: Authenticated [`@octokit/graphql`](https://github.com/octokit/graphql.js) instance, scoped to a specific GitHub App installation 49 | 50 | ## Examples 51 | 52 | ### Open a new issue 53 | 54 | ```javascript 55 | await installations[owner].octokit.issues.create({owner, repo, title}) 56 | ``` 57 | 58 | ### Retrieve the last 3 issues from a repo 59 | 60 | ```javascript 61 | await installations[owner].graphql(`query lastIssues($owner: String!, $repo: String!, $num: Int = 3) { 62 | repository(owner:$owner, name:$repo) { 63 | issues(last:$num) { 64 | edges { 65 | node { 66 | title 67 | } 68 | } 69 | } 70 | } 71 | }`, { 72 | owner: 'octokit', 73 | repo: 'graphql.js' 74 | } 75 | }) 76 | ``` 77 | 78 | ## Credits 79 | 80 | 1. [Mastering the Node.js REPL (part 2) – Trabe – Medium](https://medium.com/trabe/mastering-the-node-js-repl-part-2-365c52a5203d) 81 | -------------------------------------------------------------------------------- /github-app-repl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --experimental-repl-await 2 | require('dotenv').config(); 3 | 4 | const repl = require('repl'); 5 | const path = require('path'); 6 | const homedir = require('os').homedir(); 7 | 8 | const App = require('./lib/app'); 9 | const { findPrivateKey } = require('./lib/private-key'); 10 | 11 | // Context initializer 12 | const initializeContext = async context => { 13 | // Instantiate a new app 14 | const app = new App({ id: process.env.APP_ID, privateKey: findPrivateKey() }); 15 | await app.load(); 16 | 17 | // Get installations 18 | const installations = await app.installations(); 19 | 20 | // Extent context, with app, and installations 21 | Object.entries({ app, installations }).forEach(([k, v]) => { 22 | Object.defineProperty(context, k, { 23 | configurable: false, 24 | enumerable: true, 25 | value: v 26 | }); 27 | }); 28 | }; 29 | 30 | (async () => { 31 | // Start a repl 32 | const r = repl.start('> '); 33 | 34 | // Initialize 35 | await initializeContext(r.context); 36 | 37 | // Listen for the reset event 38 | r.on('reset', initializeContext); 39 | 40 | // Initialize a history log file 41 | r.setupHistory(path.join(homedir, '.node_repl_history'), () => {}); 42 | })(); 43 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require('@octokit/rest'); 2 | const { createAppAuth } = require('@octokit/auth-app'); 3 | const { githubAppJwt } = require('universal-github-app-jwt'); 4 | const Installation = require('./installation'); 5 | const { unpack } = require('./utils'); 6 | 7 | class App { 8 | constructor({ id, privateKey }) { 9 | this.id = parseInt(id, 10); 10 | this.privateKey = privateKey; 11 | } 12 | 13 | async jwt() { 14 | const { token } = await githubAppJwt({ 15 | id: this.id, 16 | privateKey: this.privateKey 17 | }); 18 | return token; 19 | } 20 | 21 | get octokit() { 22 | return new Octokit({ 23 | authStrategy: createAppAuth, 24 | auth: { 25 | id: this.id, 26 | privateKey: this.privateKey 27 | } 28 | }); 29 | } 30 | 31 | async load() { 32 | const { data } = await this.octokit.apps.getAuthenticated({}); 33 | unpack(data, propName => { 34 | this[propName] = data[propName]; 35 | }); 36 | return data; 37 | } 38 | 39 | async installations() { 40 | const list = await this.octokit.paginate('GET /app/installations', { 41 | headers: { 42 | accept: 'application/vnd.github.machine-man-preview+json' 43 | } 44 | }); 45 | return list.reduce( 46 | (o, installation) => Object.assign(o, { [installation.account.login]: new Installation(this, installation) }), 47 | {} 48 | ); 49 | } 50 | } 51 | 52 | module.exports = App; 53 | -------------------------------------------------------------------------------- /lib/installation.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require('@octokit/rest'); 2 | const { createAppAuth } = require('@octokit/auth-app'); 3 | const { graphql } = require('@octokit/graphql'); 4 | const { unpack } = require('./utils'); 5 | 6 | class Installation { 7 | constructor(app, params) { 8 | this.app = app; 9 | unpack(params, propName => { 10 | this[propName] = params[propName]; 11 | }); 12 | } 13 | 14 | get auth() { 15 | return { 16 | type: 'installation', 17 | installationId: this.id, 18 | privateKey: this.app.privateKey, 19 | id: this.app.id 20 | }; 21 | } 22 | 23 | get octokit() { 24 | // Create new Octokit instance that is authenticated as a GitHub App installation 25 | return new Octokit({ 26 | authStrategy: createAppAuth, 27 | auth: this.auth 28 | }); 29 | } 30 | 31 | async graphql(query, options = {}) { 32 | const auth = createAppAuth(this.auth); 33 | const client = graphql.defaults({ 34 | request: { 35 | hook: auth.hook 36 | } 37 | }); 38 | return client(query, options); 39 | } 40 | } 41 | 42 | module.exports = Installation; 43 | -------------------------------------------------------------------------------- /lib/private-key.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | /** 4 | * Finds a private key through various user-(un)specified methods. 5 | * Order of precedence: 6 | * 1. Explicit path (CLI option) 7 | * 2. `PRIVATE_KEY` env var 8 | * 3. `PRIVATE_KEY_PATH` env var 9 | * 4. Any file w/ `.pem` extension in current working dir 10 | * @param {string} [filepath] - Explicit, user-defined path to keyfile 11 | * @returns {string} Private key 12 | * @private 13 | */ 14 | function findPrivateKey(filepath) { 15 | if (filepath) { 16 | return fs.readFileSync(filepath); 17 | } 18 | if (process.env.PRIVATE_KEY) { 19 | return process.env.PRIVATE_KEY.replace(/\\n/g, '\n'); 20 | } 21 | if (process.env.PRIVATE_KEY_PATH) { 22 | return fs.readFileSync(process.env.PRIVATE_KEY_PATH); 23 | } 24 | const foundPath = fs.readdirSync(process.cwd()).find(path => path.endsWith('.pem')); 25 | if (foundPath) { 26 | return findPrivateKey(foundPath); 27 | } 28 | throw new Error(`Missing private key for GitHub App. Please use: 29 | * \`--private-key=/path/to/private-key\` flag, or 30 | * \`PRIVATE_KEY\` environment variable, or 31 | * \`PRIVATE_KEY_PATH\` environment variable 32 | `); 33 | } 34 | 35 | module.exports = { 36 | findPrivateKey 37 | }; 38 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const unpack = (o, handler) => { 2 | // eslint-disable-next-line no-restricted-syntax 3 | for (const propName of Object.getOwnPropertyNames(o)) { 4 | handler(propName); 5 | } 6 | }; 7 | 8 | module.exports.unpack = unpack; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-app-repl", 3 | "version": "1.0.1", 4 | "description": "A REPL (read–eval–print loop) for GitHub Apps", 5 | "author": "Steve Winton (https://github.com/swinton)", 6 | "main": "github-app-repl.js", 7 | "scripts": { 8 | "start": "node --experimental-repl-await ./github-app-repl.js", 9 | "test": "jest --coverage", 10 | "lint": "eslint github-app-repl.js ./github-app-repl.js lib/**.js" 11 | }, 12 | "bin": { 13 | "github-app-repl": "./github-app-repl.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/swinton/github-app-repl.git" 18 | }, 19 | "keywords": [ 20 | "github", 21 | "github-apps", 22 | "repl" 23 | ], 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/swinton/github-app-repl/issues" 27 | }, 28 | "homepage": "https://github.com/swinton/github-app-repl#readme", 29 | "dependencies": { 30 | "@octokit/auth-app": "^2.4.5", 31 | "@octokit/graphql": "^4.3.1", 32 | "@octokit/rest": "^17.7.0", 33 | "dotenv": "^8.2.0", 34 | "universal-github-app-jwt": "^1.0.2" 35 | }, 36 | "devDependencies": { 37 | "eslint": "^5.16.0", 38 | "eslint-config-airbnb": "^17.1.1", 39 | "eslint-config-prettier": "^3.6.0", 40 | "eslint-plugin-import": "^2.16.0", 41 | "eslint-plugin-jsx-a11y": "^6.2.1", 42 | "eslint-plugin-prettier": "^2.7.0", 43 | "eslint-plugin-react": "^7.12.4", 44 | "jest": "^24.9.0", 45 | "prettier": "^1.19.1" 46 | }, 47 | "engines": { 48 | "node": "12.x" 49 | }, 50 | "jest": { 51 | "testEnvironment": "node", 52 | "collectCoverageFrom": [ 53 | "github-app-repl.js", 54 | "lib/*.js" 55 | ], 56 | "coverageThreshold": { 57 | "global": { 58 | "branches": 80, 59 | "functions": 80, 60 | "lines": 80, 61 | "statements": 80 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /script/historytailnode: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Get sleep time in seconds, default to 1 second 4 | sleeptimesecs=${1:-1} 5 | 6 | # Get number of lines to print, default to 20 7 | numberoflines=${2:-20} 8 | 9 | while [ 1 ]; do 10 | clear 11 | head -n $numberoflines ~/.node_repl_history 12 | sleep $sleeptimesecs 13 | done 14 | --------------------------------------------------------------------------------