├── .all-contributorsrc ├── .editorconfig ├── .github ├── renovate.json ├── stale.yml └── workflows │ ├── release.yml │ ├── test.yml │ └── update-prettier.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── script ├── make-executable.js ├── setup.sh ├── test-all-templates.sh └── test-template.sh ├── src ├── create-probot-app.ts ├── helpers │ ├── filesystem.ts │ ├── init-git.ts │ ├── run-npm.ts │ ├── user-interaction.ts │ └── write-help.ts ├── run-tests.ts └── types │ ├── conjecture.d.ts │ ├── egad.d.ts │ └── stringify-author.d.ts ├── templates ├── __common__ │ ├── .dockerignore │ ├── .env.example │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── gitignore │ └── test │ │ └── fixtures │ │ └── mock-cert.pem ├── basic-js │ ├── __description__.txt │ ├── app.yml │ ├── index.js │ ├── package.json │ └── test │ │ ├── fixtures │ │ └── issues.opened.json │ │ └── index.test.js ├── basic-ts │ ├── __description__.txt │ ├── app.yml │ ├── package.json │ ├── src │ │ └── index.ts │ ├── test │ │ ├── fixtures │ │ │ └── issues.opened.json │ │ └── index.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── checks-js │ ├── __description__.txt │ ├── app.yml │ ├── index.js │ ├── package.json │ └── test │ │ ├── fixtures │ │ ├── check_run.created.json │ │ └── check_suite.requested.json │ │ └── index.test.js ├── deploy-js │ ├── __description__.txt │ ├── app.yml │ ├── index.js │ ├── package.json │ └── test │ │ ├── fixtures │ │ └── pull_request.opened.json │ │ └── index.test.js └── git-data-js │ ├── __description__.txt │ ├── app.yml │ ├── index.js │ ├── package.json │ └── test │ ├── fixtures │ └── installation.created.json │ └── index.test.js ├── tsconfig.all.json └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "MaximDevoir", 10 | "name": "Maxim Devoir", 11 | "avatar_url": "https://avatars3.githubusercontent.com/u/10104630?v=4", 12 | "profile": "https://create-nom.app", 13 | "contributions": [ 14 | "code", 15 | "review" 16 | ] 17 | }, 18 | { 19 | "login": "shaftoe", 20 | "name": "Alexander Fortin", 21 | "avatar_url": "https://avatars1.githubusercontent.com/u/281389?v=4", 22 | "profile": "https://a.l3x.in/", 23 | "contributions": [ 24 | "code" 25 | ] 26 | }, 27 | { 28 | "login": "hiimbex", 29 | "name": "Bex Warner", 30 | "avatar_url": "https://avatars1.githubusercontent.com/u/13410355?v=4", 31 | "profile": "http://hiimbex.com", 32 | "contributions": [ 33 | "code", 34 | "review" 35 | ] 36 | }, 37 | { 38 | "login": "tcbyrd", 39 | "name": "Tommy Byrd", 40 | "avatar_url": "https://avatars0.githubusercontent.com/u/13207348?v=4", 41 | "profile": "https://github.com/tcbyrd", 42 | "contributions": [ 43 | "code" 44 | ] 45 | }, 46 | { 47 | "login": "JasonEtco", 48 | "name": "Jason Etcovitch", 49 | "avatar_url": "https://avatars1.githubusercontent.com/u/10660468?v=4", 50 | "profile": "https://jasonet.co", 51 | "contributions": [ 52 | "code" 53 | ] 54 | } 55 | ], 56 | "contributorsPerLine": 7, 57 | "projectName": "create-probot-app", 58 | "projectOwner": "probot", 59 | "repoType": "github", 60 | "repoHost": "https://github.com", 61 | "skipCi": true 62 | } 63 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>probot/.github"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for https://github.com/probot/stale 2 | _extends: .github 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | "on": 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: lts/* 15 | cache: npm 16 | - run: npm ci 17 | - run: npm run build --if-present 18 | - run: npx semantic-release 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | "on": 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | test_matrix: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node: 16 | - 18 17 | - 20 18 | - 21 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node }} 24 | cache: npm 25 | - run: npm ci 26 | - run: npm run build 27 | - run: npm run test 28 | test: 29 | needs: test_matrix 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: 18 36 | cache: npm 37 | - run: npm install 38 | - run: npm run lint 39 | -------------------------------------------------------------------------------- /.github/workflows/update-prettier.yml: -------------------------------------------------------------------------------- 1 | name: Update Prettier 2 | "on": 3 | push: 4 | branches: 5 | - dependabot/npm_and_yarn/prettier-* 6 | jobs: 7 | update_prettier: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | cache: npm 14 | node-version: 16 15 | - run: npm ci 16 | - run: npm run lint:fix 17 | - uses: gr2m/create-or-update-pull-request-action@v1.x 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | with: 21 | title: Prettier updated 22 | body: An update to prettier required updates to your code. 23 | branch: ${{ github.ref }} 24 | commit-message: "style: prettier" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Output folder 5 | bin 6 | 7 | # Logs 8 | *.log 9 | 10 | # Yarn lockfile. Use package-lock.json instead. 11 | yarn.lock 12 | 13 | # Test-related generated folders 14 | coverage 15 | 16 | # Miscellaneous 17 | .DS_Store 18 | Thumbs.db 19 | .vscode 20 | __create_probot_app__* 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017, Brandon Keepers 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 | # Create Probot App 2 | 3 | [![CI](https://github.com/probot/create-probot-app/workflows/Test/badge.svg)](https://github.com/probot/create-probot-app/actions) 4 | 5 | `create-probot-app` is a _command line_ (CLI) Node.js application that generates a new [Probot](https://github.com/probot/probot) app with everything you need to get started building. 👷🏽‍ 6 | 7 | More specifically, this command line interface allows you to select from our pre-defined blueprints to choose a basic working example to start from. 8 | 9 | ## Installation 10 | 11 | Make sure you've got [Node.js installed](https://Node.js.org/en/download/) on your workstation, then open your terminal and type the following command: 12 | 13 | - if you're using `npm` (the package manager bundled with `Node.js`): 14 | 15 | ```sh 16 | npx create-probot-app my-first-app 17 | ``` 18 | 19 | - if you're using Yarn: 20 | 21 | ```sh 22 | yarn create probot-app my-first-app 23 | ``` 24 | 25 | and follow the instructions printed on the terminal as you go. `create-probot-app` will then take care of the heavy lifting required to setup a Probot app development environment, with proper folder structure, and even installing all the basic `Probot` dependencies. 26 | 27 | ## How to run locally 28 | 29 | See the [Probot docs](https://probot.github.io/docs/development/#running-the-app-locally) to get started running your app locally. 30 | 31 | ## Contributors ✨ 32 | 33 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |

Maxim Devoir

💻 👀

Alexander Fortin

💻

Bex Warner

💻 👀

Tommy Byrd

💻

Jason Etcovitch

💻
47 | 48 | 49 | 50 | 51 | 52 | 53 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-probot-app", 3 | "version": "0.0.0-development", 4 | "description": "Create a Probot app", 5 | "bin": { 6 | "create-probot-app": "./bin/create-probot-app.js", 7 | "run-tests": "./bin/run-tests.js" 8 | }, 9 | "files": [ 10 | "bin", 11 | "templates" 12 | ], 13 | "scripts": { 14 | "test": "./script/test-all-templates.sh", 15 | "test:template": "./script/test-template.sh", 16 | "lint": "prettier --ignore-path .gitignore --check '**/*.{js,ts,json,yml,md}'", 17 | "lint:fix": "prettier --ignore-path .gitignore --write '**/*.{js,ts,json,yml,md}'", 18 | "dev:make-cpa": "node --input-type=module -e 'import { chBinMod } from \"./script/make-executable.js\"; chBinMod(\"create-probot-app\")'", 19 | "dev:make-tests": "node --input-type=module -e 'import { chBinMod } from \"./script/make-executable.js\"; chBinMod(\"run-tests\")'", 20 | "build": "npm run build:clean && tsc && npm run dev:make-cpa && npm run dev:make-tests", 21 | "build:clean": "rimraf bin", 22 | "build:source": "tsc && npm run dev:make-cpa", 23 | "build:tests": "tsc && npm run dev:make-tests" 24 | }, 25 | "repository": "github:probot/create-probot-app", 26 | "keywords": [ 27 | "probot" 28 | ], 29 | "author": "Brandon Keepers", 30 | "license": "ISC", 31 | "dependencies": { 32 | "chalk": "^5.2.0", 33 | "commander": "^12.0.0", 34 | "conjecture": "^0.1.2", 35 | "egad": "^0.2.0", 36 | "execa": "^8.0.1", 37 | "inquirer": "^9.1.4", 38 | "jsesc": "^3.0.2", 39 | "lodash.camelcase": "^4.3.0", 40 | "lodash.kebabcase": "^4.1.1", 41 | "simple-git": "^3.22.0", 42 | "stringify-author": "^0.1.3", 43 | "validate-npm-package-name": "^5.0.0" 44 | }, 45 | "devDependencies": { 46 | "@types/cross-spawn": "^6.0.2", 47 | "@types/inquirer": "^9.0.3", 48 | "@types/jsesc": "^3.0.1", 49 | "@types/lodash.camelcase": "^4.3.7", 50 | "@types/lodash.kebabcase": "^4.1.7", 51 | "@types/node": "^20.0.0", 52 | "@types/rimraf": "^4.0.5", 53 | "@types/validate-npm-package-name": "^4.0.0", 54 | "prettier": "^3.2.4", 55 | "rimraf": "^5.0.5", 56 | "semantic-release": "^24.0.0", 57 | "typescript": "^5.3.3" 58 | }, 59 | "type": "module", 60 | "engines": { 61 | "node": ">= 18" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /script/make-executable.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | import pkg from "../package.json" with { type: "json" }; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | const TYPE_MASK = parseInt("0770000", 8); 10 | 11 | /** 12 | * Converts TS file under ./bin/ into an executable file. 13 | * 14 | * By default, the compiled `bin/*.js` scripts ar not executable. 15 | * When a developer is modifying the application, they won't be able to run the 16 | * compiled scripts from the CLI. 17 | * 18 | * This utility function applies executable permissions to the compiled binary script. 19 | * 20 | * @param {string} name the name of the built JS file, e.g. 'create-probot-app' 21 | */ 22 | export function chBinMod(name) { 23 | const binList = pkg.bin; 24 | const jsFilePath = binList[name]; 25 | const distributableBinary = path.join(__dirname, "..", jsFilePath); 26 | 27 | try { 28 | if (fs.existsSync(distributableBinary)) { 29 | const currentMode = fs.statSync(distributableBinary).mode; 30 | let execMode = currentMode | ((currentMode >>> 2) & TYPE_MASK); 31 | // Add execute permissions for owner, group, and others. 32 | execMode |= 0o111; 33 | fs.chmodSync(distributableBinary, execMode); 34 | console.log(`Converted ${name} to an executable binary.`); 35 | } 36 | } catch (err) { 37 | console.error(err); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /script/setup.sh: -------------------------------------------------------------------------------- 1 | set -Ee # Exit immediately if a command returns a non-zero status 2 | set -u # Exit when references variables are undefined 3 | set -o pipefail # Exit when any program execution in a pipeline breaks 4 | -------------------------------------------------------------------------------- /script/test-all-templates.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | . ./script/setup.sh 3 | 4 | TEMPLATES=$(./bin/create-probot-app.js --show-templates ALL) 5 | 6 | for template in $TEMPLATES; do 7 | npm run test:template $template; 8 | done 9 | -------------------------------------------------------------------------------- /script/test-template.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | . ./script/setup.sh 3 | 4 | readonly APP="./bin/create-probot-app.js" 5 | readonly TEMPLATE=$1 6 | readonly TEST_FOLDER=$(mktemp -d -t cpa-XXXXXXXXXX) 7 | readonly LOGFILENAME="test.output" 8 | readonly LOGFILE="${TEST_FOLDER}/${LOGFILENAME}" 9 | 10 | function create_app() { 11 | mkdir -p "$TEST_FOLDER" 12 | "$APP" \ 13 | --appName "template-test-app" \ 14 | --desc "A Probot App for building on Travis" \ 15 | --author "Pro Báwt" \ 16 | --email "probot@example.com" \ 17 | --user "probot" \ 18 | --template "$TEMPLATE" \ 19 | --repo "create-probot-app-${TEMPLATE}" \ 20 | "$TEST_FOLDER" 21 | } 22 | 23 | function run_npm_tests() { 24 | echo; echo "--[test ${TEMPLATE}]-- Run npm tests... " 25 | cd "$TEST_FOLDER" 26 | npm test 2>&1 | tee "$LOGFILENAME" 27 | cd - > /dev/null 28 | } 29 | 30 | echo "--[test ${TEMPLATE}]-- Run tests in ${TEST_FOLDER} folder" 31 | create_app 32 | run_npm_tests 33 | ./bin/run-tests.js $TEMPLATE $TEST_FOLDER 34 | echo "--[test ${TEMPLATE}]-- All tests completed successfully!" 35 | -------------------------------------------------------------------------------- /src/create-probot-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { askUser, runCliManager } from "./helpers/user-interaction.js"; 3 | import { initGit } from "./helpers/init-git.js"; 4 | import { installAndBuild } from "./helpers/run-npm.js"; 5 | import { makeScaffolding } from "./helpers/filesystem.js"; 6 | import { printSuccess, red } from "./helpers/write-help.js"; 7 | 8 | runCliManager() 9 | .then((cliConfig) => askUser(cliConfig)) 10 | .then((config) => makeScaffolding(config)) 11 | .then(async (config) => { 12 | if (config.gitInit) await initGit(config.destination); 13 | return config; 14 | }) 15 | .then(async (config) => await installAndBuild(config)) 16 | .then((config) => printSuccess(config.appName, config.destination)) 17 | .catch((err) => { 18 | console.log(red(err)); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /src/helpers/filesystem.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import * as fs from "node:fs"; 4 | import { generate } from "egad"; 5 | import { Config } from "./user-interaction.js"; 6 | import { yellow, green } from "./write-help.js"; 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | export const templatesSourcePath = path.join(__dirname, "../../templates/"); 11 | 12 | /** 13 | * Validate `destination` path, throw error if not valid (e.g not an empty folder) 14 | * @param {string} destination destination path 15 | * @param {boolean} overwrite if true, don't throw error if path is a non-empty valid folder 16 | */ 17 | export function ensureValidDestination( 18 | destination: string, 19 | overwrite: boolean, 20 | ): void { 21 | const invalidDestinationError: Error = 22 | new Error(`Invalid destination folder => ${destination} 23 | Please provide either an empty folder or a non existing path as 24 | `); 25 | 26 | try { 27 | fs.lstatSync(destination); 28 | 29 | try { 30 | fs.realpathSync(destination); 31 | } catch (error: any) { 32 | if (error.code === "ENOENT") throw invalidDestinationError; // Edge case: destination is a broken link 33 | } 34 | } catch (error: any) { 35 | if (error.code === "ENOENT") return; 36 | // The `destination` is neither a broken link nor an existing path 37 | else throw error; 38 | } 39 | 40 | if (fs.statSync(destination).isDirectory()) { 41 | if (fs.readdirSync(destination).length === 0) return; // Empty folder 42 | if (overwrite) { 43 | console.warn( 44 | yellow(`Explicit OVERWRITE option selected: 45 | Some files under "${fs.realpathSync(destination)}" might be overwritten! 46 | `), 47 | ); 48 | return; 49 | } 50 | } 51 | 52 | // The `destination` is neither a valid folder nor an empty one 53 | throw invalidDestinationError; 54 | } 55 | 56 | /** 57 | * Create files and folder structure from Handlebars templates. 58 | * 59 | * @param {Config} config configuration object 60 | * @returns Promise which returns the input Config object 61 | */ 62 | export async function makeScaffolding(config: Config): Promise { 63 | // Prepare template folder Handlebars source content merging `templates/__common__` and `templates/` 64 | const tempDestPath = fs.mkdtempSync("__create_probot_app__"); 65 | [ 66 | path.join(templatesSourcePath, "__common__"), 67 | path.join(templatesSourcePath, config.template), 68 | ].forEach((source) => 69 | fs.cpSync(source, tempDestPath, { 70 | recursive: true, 71 | }), 72 | ); 73 | 74 | fs.rmSync(path.join(tempDestPath, "__description__.txt")); 75 | 76 | if (fs.existsSync(path.join(tempDestPath, "gitignore"))) 77 | fs.renameSync( 78 | path.join(tempDestPath, "gitignore"), 79 | path.join(tempDestPath, ".gitignore"), 80 | ); 81 | 82 | const result = await generate(tempDestPath, config.destination, config, { 83 | overwrite: config.overwrite, 84 | }); 85 | 86 | fs.rmSync(tempDestPath, { 87 | recursive: true, 88 | }); 89 | 90 | result.forEach((fileInfo) => { 91 | console.log( 92 | `${ 93 | fileInfo.skipped 94 | ? yellow("skipped existing file") 95 | : green("created file") 96 | }: ${fileInfo.path}`, 97 | ); 98 | }); 99 | 100 | console.log(green("\nFinished scaffolding files!")); 101 | return config; 102 | } 103 | 104 | interface Template { 105 | name: string; 106 | description: string; 107 | } 108 | 109 | export function getTemplates(): Template[] { 110 | return fs 111 | .readdirSync(templatesSourcePath) 112 | .filter((path) => path.substr(0, 2) !== "__") 113 | .map((template) => { 114 | let descFile = path.join( 115 | templatesSourcePath, 116 | template, 117 | "__description__.txt", 118 | ); 119 | return { 120 | name: template, 121 | description: fs.readFileSync(descFile).toString().trimEnd(), 122 | }; 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /src/helpers/init-git.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import { execa } from "execa"; 4 | import simplegit from "simple-git"; 5 | 6 | import { green, yellow, red } from "./write-help.js"; 7 | 8 | function isInGitRepo(path: string): boolean { 9 | const gitRevParse = execa("git", ["rev-parse", "--is-inside-work-tree"], { 10 | cwd: path, 11 | stdio: "ignore", 12 | }); 13 | 14 | if (gitRevParse.exitCode === 0) { 15 | console.log("Found already initialized Git repository"); 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | function isGitInstalled(): boolean { 22 | try { 23 | execa("git", ["--version"], { 24 | stdio: "ignore", 25 | }); 26 | } catch (error: any) { 27 | console.log("`git` binary not found"); 28 | return false; 29 | } 30 | return true; 31 | } 32 | 33 | /** 34 | * Initialize a Git repository in target destination folder 35 | * 36 | * @param {String} destination the destination folder path 37 | */ 38 | export async function initGit(destination: string): Promise { 39 | let initializedGit = false; 40 | 41 | console.log(`\nInitializing Git repository in folder '${destination}'`); 42 | 43 | if (!isGitInstalled() || isInGitRepo(destination)) { 44 | console.log(yellow("Skipping Git initialization")); 45 | return; 46 | } 47 | 48 | const git = simplegit(destination); 49 | 50 | try { 51 | await git 52 | .init() 53 | .then(() => (initializedGit = true)) 54 | .then(() => git.add("./*")) 55 | .then(() => git.commit("Initial commit from Create Probot App")) 56 | .then(() => console.log(green("Initialized a Git repository"))); 57 | } catch (error) { 58 | if (initializedGit) { 59 | const gitFolder = path.join(destination, ".git"); 60 | console.log(red(`Cleaning up ${gitFolder} folder`)); 61 | try { 62 | fs.rmdirSync(gitFolder); 63 | } catch {} 64 | } 65 | console.log(red(`Errors while initializing Git repo: ${error}`)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/helpers/run-npm.ts: -------------------------------------------------------------------------------- 1 | import { bold, yellow } from "./write-help.js"; 2 | import { Config } from "./user-interaction.js"; 3 | import { execa } from "execa"; 4 | 5 | export function detectPackageManager(): string { 6 | const { npm_config_user_agent: userAgent } = process.env; 7 | if (userAgent) { 8 | if (userAgent.includes("yarn")) return "yarn"; 9 | if (userAgent.includes("npm")) return "npm"; 10 | } 11 | return "npm"; 12 | } 13 | 14 | /** 15 | * Run `npm install` in `destination` folder, then run `npm run build` 16 | * if `toBuild` is true 17 | * 18 | * @param {Config} config configuration object 19 | * 20 | * @returns Promise which returns the input Config object 21 | */ 22 | export async function installAndBuild(config: Config): Promise { 23 | const originalDir = process.cwd(); 24 | process.chdir(config.destination); 25 | const packageManager = detectPackageManager(); 26 | console.log( 27 | yellow("\nInstalling dependencies. This may take a few minutes...\n"), 28 | ); 29 | try { 30 | await execa(packageManager, ["install"]); 31 | } catch (error: any) { 32 | process.chdir(originalDir); 33 | throw new Error( 34 | `\nCould not install npm dependencies.\nTry running ${bold( 35 | "npm install", 36 | )} yourself.\n`, 37 | ); 38 | } 39 | if (config.toBuild) { 40 | console.log(yellow("\n\nCompile application...\n")); 41 | try { 42 | await execa(packageManager, ["run", "build"]); 43 | } catch (error: any) { 44 | process.chdir(originalDir); 45 | throw new Error( 46 | `\nCould not build application.\nTry running ${bold( 47 | "npm run build", 48 | )} yourself.\n`, 49 | ); 50 | } 51 | } 52 | process.chdir(originalDir); 53 | return config; 54 | } 55 | -------------------------------------------------------------------------------- /src/helpers/user-interaction.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import * as path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | import { guessEmail, guessGitHubUsername, guessAuthor } from "conjecture"; 6 | import camelCase from "lodash.camelcase"; 7 | import * as commander from "commander"; 8 | import inquirer, { Answers, Question, QuestionCollection } from "inquirer"; 9 | import jsesc from "jsesc"; 10 | import kebabCase from "lodash.kebabcase"; 11 | import stringifyAuthor from "stringify-author"; 12 | import validatePackageName from "validate-npm-package-name"; 13 | 14 | import { blue, red, printHelpAndFail } from "./write-help.js"; 15 | import { getTemplates, ensureValidDestination } from "./filesystem.js"; 16 | 17 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 18 | 19 | const templateDelimiter = " => "; 20 | 21 | type QuestionI = 22 | | ( 23 | | { 24 | default?(answers: Answers): string | boolean; 25 | } 26 | | Question 27 | ) 28 | | QuestionCollection; 29 | 30 | interface CliConfig { 31 | destination: string; 32 | gitInit: boolean; 33 | overwrite: boolean; 34 | appName?: string; 35 | author?: string; 36 | description?: string; 37 | email?: string; 38 | repo?: string; 39 | template?: string; 40 | user?: string; 41 | } 42 | 43 | export interface Config extends CliConfig, Answers { 44 | appName: string; 45 | camelCaseAppName: string; 46 | description: string; 47 | template: string; 48 | toBuild: boolean; 49 | year: number; 50 | owner?: string; 51 | } 52 | 53 | /** 54 | * Partially sanitizes keys by escaping double-quotes. 55 | * 56 | * @param {Object} object The object to mutate. 57 | * @param {String[]} keys The keys on `object` to sanitize. 58 | */ 59 | function sanitizeBy(object: Config, keys: string[]): void { 60 | keys.forEach((key) => { 61 | if (key in object) { 62 | object[key] = jsesc(object[key], { 63 | minimal: true, 64 | quotes: "double", 65 | }); 66 | } 67 | }); 68 | } 69 | 70 | function getQuestions(config: CliConfig): QuestionI[] { 71 | const templates = getTemplates(); 72 | 73 | const questions: QuestionI[] = [ 74 | { 75 | type: "input", 76 | name: "appName", 77 | default(answers: Answers): string { 78 | return answers.repo || kebabCase(path.basename(config.destination)); 79 | }, 80 | message: "App name:", 81 | when: !config.appName, 82 | validate(appName): true | string { 83 | const result = validatePackageName(appName); 84 | if (result.errors && result.errors.length > 0) { 85 | return result.errors.join(","); 86 | } 87 | 88 | return true; 89 | }, 90 | }, 91 | { 92 | type: "input", 93 | name: "description", 94 | default(): string { 95 | return "A Probot app"; 96 | }, 97 | message: "Description of app:", 98 | when: !config.description, 99 | }, 100 | { 101 | type: "input", 102 | name: "author", 103 | default(): string { 104 | return guessAuthor(); 105 | }, 106 | message: "Author's full name:", 107 | when: !config.author, 108 | }, 109 | { 110 | type: "input", 111 | name: "email", 112 | default(): Promise { 113 | return guessEmail(); 114 | }, 115 | message: "Author's email address:", 116 | when: config.gitInit && !config.email, 117 | }, 118 | { 119 | type: "input", 120 | name: "user", 121 | default(answers: Answers): Promise { 122 | return guessGitHubUsername(answers.email); 123 | }, 124 | message: "GitHub user or org name:", 125 | when: config.gitInit && !config.user, 126 | }, 127 | { 128 | type: "input", 129 | name: "repo", 130 | default(answers: Answers): string { 131 | return answers.appName || kebabCase(path.basename(config.destination)); 132 | }, 133 | message: "Repository name:", 134 | when: config.gitInit && !config.repo, 135 | }, 136 | { 137 | type: "list", 138 | name: "template", 139 | choices: templates.map( 140 | (template) => 141 | `${template.name}${templateDelimiter}${template.description}`, 142 | ), 143 | message: "Which template would you like to use?", 144 | when(): boolean { 145 | if (config.template) { 146 | if (templates.find((template) => template.name === config.template)) { 147 | return false; 148 | } 149 | console.log( 150 | red(`The template ${blue(config.template)} does not exist.`), 151 | ); 152 | } 153 | return true; 154 | }, 155 | }, 156 | ]; 157 | 158 | return questions; 159 | } 160 | 161 | /** 162 | * Prompt the user for mandatory options not set via CLI 163 | * 164 | * @param config Configuration data already set via CLI options 165 | * 166 | * @returns the merged configuration options from CLI and user prompt 167 | */ 168 | export async function askUser(config: CliConfig): Promise { 169 | console.log( 170 | "\nLet's create a Probot app!\nHit enter to accept the suggestion.\n", 171 | ); 172 | 173 | const answers = { 174 | ...config, 175 | ...((await inquirer.prompt(getQuestions(config))) as Config), 176 | }; 177 | 178 | // enrich with (meta)data + sanitize input 179 | answers.author = stringifyAuthor({ 180 | name: answers.author, 181 | email: answers.email, 182 | }); 183 | answers.toBuild = answers.template.slice(-3) === "-ts"; 184 | answers.year = new Date().getFullYear(); 185 | answers.camelCaseAppName = camelCase(config.appName || answers.appName); 186 | answers.owner = answers.user; 187 | answers.template = answers.template.split(templateDelimiter)[0]; // remove eventual description 188 | sanitizeBy(answers, ["author", "description"]); 189 | 190 | return answers; 191 | } 192 | 193 | /** 194 | * Run CLI manager to parse user provided options and arguments 195 | * 196 | * @returns resolves with the configuration options set via CLI 197 | */ 198 | export async function runCliManager(): Promise { 199 | let destination: string = ""; 200 | 201 | // TSC mangles output directory when using normal import methods for 202 | // package.json. See 203 | // https://github.com/Microsoft/TypeScript/issues/24715#issuecomment-542490675 204 | const pkg = JSON.parse( 205 | fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf-8"), 206 | ); 207 | 208 | const program = new commander.Command("create-probot-app") 209 | .arguments("[destination]") 210 | .action((dest) => { 211 | if (dest) { 212 | destination = path.isAbsolute(dest) 213 | ? dest 214 | : path.resolve(process.cwd(), dest); 215 | } 216 | }) 217 | .usage("[options] ") 218 | .option("-n, --appName ", "App name") 219 | .option('-d, --desc ""', "Description (contain in quotes)") 220 | .option('-a, --author ""', "Author name (contain in quotes)") 221 | .option("-e, --email ", "Author email address") 222 | .option("-u, --user ", "GitHub username or org (repo owner)") 223 | .option("-r, --repo ", "Repository name") 224 | .option("--overwrite", "Overwrite existing files", false) 225 | .option("-t, --template