diff --git a/package-lock.json b/package-lock.json index ae728a4..1f2651b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,14 +13,16 @@ "@segment/analytics-node": "^1.0.0-beta.26", "axios": "^1.4.0", "axios-debug-log": "^1.0.0", - "bun": "^1.1.21", "chalk": "^5.2.0", "cli-table": "^0.3.11", "crypto-random-string": "^5.0.0", "diff": "^5.2.0", "inquirer": "^9.2.6", + "mustache": "^4.2.0", + "octokit": "^4.0.2", "open": "^10.1.0", "openid-client": "^5.6.5", + "p-limit": "^6.1.0", "prompts": "2.4.2", "validate-npm-package-name": "5.0.1", "which": "^3.0.1", @@ -46,6 +48,7 @@ "@types/diff": "^5.2.1", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.17", + "@types/mustache": "^4.2.5", "@types/node": "^18.7.13", "@types/prompts": "2.4.9", "@types/validate-npm-package-name": "4.0.2", @@ -972,17 +975,118 @@ "node": ">= 8" } }, + "node_modules/@octokit/app": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-15.1.0.tgz", + "integrity": "sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^7.0.0", + "@octokit/auth-unauthenticated": "^6.0.0", + "@octokit/core": "^6.1.2", + "@octokit/oauth-app": "^7.0.0", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/types": "^13.0.0", + "@octokit/webhooks": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-7.1.1.tgz", + "integrity": "sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.1.0", + "@octokit/auth-oauth-user": "^5.1.0", + "@octokit/request": "^9.1.1", + "@octokit/request-error": "^6.1.1", + "@octokit/types": "^13.4.1", + "lru-cache": "^10.0.0", + "universal-github-app-jwt": "^2.2.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", + "integrity": "sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", + "integrity": "sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", + "integrity": "sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^7.0.1", + "@octokit/oauth-methods": "^5.0.0", + "@octokit/request": "^9.0.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@octokit/auth-token": { "version": "5.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 18" } }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-6.1.0.tgz", + "integrity": "sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==", + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@octokit/core": { "version": "6.1.2", - "dev": true, "license": "MIT", "dependencies": { "@octokit/auth-token": "^5.0.0", @@ -999,7 +1103,6 @@ }, "node_modules/@octokit/endpoint": { "version": "10.1.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^13.0.0", @@ -1011,7 +1114,6 @@ }, "node_modules/@octokit/graphql": { "version": "8.1.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/request": "^9.0.0", @@ -1022,14 +1124,88 @@ "node": ">= 18" } }, + "node_modules/@octokit/oauth-app": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-7.1.3.tgz", + "integrity": "sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^8.0.0", + "@octokit/auth-oauth-user": "^5.0.1", + "@octokit/auth-unauthenticated": "^6.0.0-beta.1", + "@octokit/core": "^6.0.0", + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/oauth-methods": "^5.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", + "integrity": "sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-5.1.2.tgz", + "integrity": "sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==", + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^7.0.0", + "@octokit/request": "^9.1.0", + "@octokit/request-error": "^6.1.0", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@octokit/openapi-types": { "version": "22.2.0", - "dev": true, "license": "MIT" }, + "node_modules/@octokit/openapi-webhooks-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-8.3.0.tgz", + "integrity": "sha512-vKLsoR4xQxg4Z+6rU/F65ItTUz/EXbD+j/d4mlq2GW8TsA4Tc8Kdma2JTAAJ5hrKWUQzkR/Esn2fjsqiVRYaQg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.2.tgz", + "integrity": "sha512-7znSVvlNAOJisCqAnjN1FtEziweOHSjPGAuc5W58NeGNAr/ZB57yCsjQbXDlWsVryA7hHQaEQPcBbJYFawlkyg==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, "node_modules/@octokit/plugin-paginate-rest": { "version": "11.3.3", - "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.5.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.4.tgz", + "integrity": "sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==", "license": "MIT", "dependencies": { "@octokit/types": "^13.5.0" @@ -1043,7 +1219,6 @@ }, "node_modules/@octokit/plugin-retry": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/request-error": "^6.0.0", @@ -1059,7 +1234,6 @@ }, "node_modules/@octokit/plugin-throttling": { "version": "9.3.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^13.0.0", @@ -1074,7 +1248,6 @@ }, "node_modules/@octokit/request": { "version": "9.1.3", - "dev": true, "license": "MIT", "dependencies": { "@octokit/endpoint": "^10.0.0", @@ -1088,7 +1261,6 @@ }, "node_modules/@octokit/request-error": { "version": "6.1.4", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^13.0.0" @@ -1099,115 +1271,33 @@ }, "node_modules/@octokit/types": { "version": "13.5.0", - "dev": true, "license": "MIT", "dependencies": { "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@oven/bun-darwin-aarch64": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.1.21.tgz", - "integrity": "sha512-n1hZewJPZg5XcubisWDaKn/wLaldgagAWya3ZuMBuFwsz4PnGTeQ7Wl3aBe7XzW6fNUAd+ZIfvfNYBRNv1R7Rw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-darwin-x64": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.1.21.tgz", - "integrity": "sha512-Vr7tz6UBrtkJ0UMCQBRhKH/JThWxkZWnGAmcGFf8h3zFgMfCaTmmWzB4PSCad1wu+4GCrmVoEG8P7MY8+TmS7w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-darwin-x64-baseline": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.1.21.tgz", - "integrity": "sha512-4MhDFYONGIg2MqO56u6H/X9TD3+hbDQpOjlGdl7J0aUiV47b3k7vLn5hENYEjAIBR3g744E23rIw4FQAXakFMw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-linux-aarch64": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.1.21.tgz", - "integrity": "sha512-0avxsNle8QOLsDwo1lqO1o2Mv1bLp3RlVr83XNV2yGVnzCwZmupQcI76fcc2e+Y+YU173xCUasMkiIbguS271g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.1.21.tgz", - "integrity": "sha512-zmps8oWLE2L+9Cn6oQPbcxIWDIjOT1txbYAv9zlcd84I12DXiB++e/PEE8dPe/3powygCpwZM9b7gZfTv9sx0w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64-baseline": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.1.21.tgz", - "integrity": "sha512-HT+PEWa2PY73gBrNuUHrihsGNOBQKp6s6IzAqHUfmDlIyXYaEvRYUZg6vEqyRRSuNcCC6PiQDHWZP99OT2VMZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-windows-x64": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.1.21.tgz", - "integrity": "sha512-p9rjwZPiJJtBafJ7MoJvmqyCA4QxVVpM7QaDx6Lhqua7b+i7dsigog8BgeCxGXAMpSKqoBuAuziqnLh0pcdAYQ==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/webhooks": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-13.3.0.tgz", + "integrity": "sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@octokit/openapi-webhooks-types": "8.3.0", + "@octokit/request-error": "^6.0.1", + "@octokit/webhooks-methods": "^5.0.0" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/@oven/bun-windows-x64-baseline": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.1.21.tgz", - "integrity": "sha512-xwPqSrcdSAJVmCnDlpvEWVHDSf9lmCBIcL5PtM9udrqTJOAVxiyQm0cpXjuv/h6MAZxt7rtt9YqrcK0ixA2xIQ==", - "cpu": [ - "x64" - ], + "node_modules/@octokit/webhooks-methods": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-5.1.0.tgz", + "integrity": "sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">= 18" + } }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", @@ -1928,6 +2018,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aws-lambda": { + "version": "8.10.145", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.145.tgz", + "integrity": "sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "dev": true, @@ -2062,6 +2158,13 @@ "version": "0.7.34", "license": "MIT" }, + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "18.19.41", "dev": true, @@ -2406,6 +2509,22 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@vitest/snapshot": { "version": "1.6.0", "dev": true, @@ -2722,7 +2841,6 @@ }, "node_modules/before-after-hook": { "version": "3.0.2", - "dev": true, "license": "Apache-2.0" }, "node_modules/bl": { @@ -2794,7 +2912,6 @@ }, "node_modules/bottleneck": { "version": "2.19.5", - "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -2819,6 +2936,8 @@ }, "node_modules/buffer": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -2850,36 +2969,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bun": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/bun/-/bun-1.1.21.tgz", - "integrity": "sha512-mvqYEvafGskIVTjlftbKvsXtyR6z/SQnhJsVw0xCU46pc56oX1sAGvaemWKOy/sy/gGMHcgLE0KUidDQQzqXWQ==", - "cpu": [ - "arm64", - "x64" - ], - "hasInstallScript": true, - "license": "MIT", - "os": [ - "darwin", - "linux", - "win32" - ], - "bin": { - "bun": "bin/bun.exe", - "bunx": "bin/bun.exe" - }, - "optionalDependencies": { - "@oven/bun-darwin-aarch64": "1.1.21", - "@oven/bun-darwin-x64": "1.1.21", - "@oven/bun-darwin-x64-baseline": "1.1.21", - "@oven/bun-linux-aarch64": "1.1.21", - "@oven/bun-linux-x64": "1.1.21", - "@oven/bun-linux-x64-baseline": "1.1.21", - "@oven/bun-windows-x64": "1.1.21", - "@oven/bun-windows-x64-baseline": "1.1.21" - } - }, "node_modules/bun-types": { "version": "1.1.17", "dev": true, @@ -6770,6 +6859,15 @@ "readable-stream": "^3.6.0" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "license": "ISC", @@ -7600,6 +7698,7 @@ "node_modules/npm/node_modules/brace-expansion": { "version": "2.0.1", "dev": true, + "inBundle": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7885,7 +7984,7 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.4.5", + "version": "10.4.2", "dev": true, "inBundle": true, "license": "ISC", @@ -7900,6 +7999,9 @@ "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -8358,15 +8460,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/npm/node_modules/minipass": { "version": "7.1.2", "dev": true, @@ -9519,6 +9612,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/octokit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-4.0.2.tgz", + "integrity": "sha512-wbqF4uc1YbcldtiBFfkSnquHtECEIpYD78YUXI6ri1Im5OO2NLo6ZVpRdbJpdnpZ05zMrVPssNiEo6JQtea+Qg==", + "license": "MIT", + "dependencies": { + "@octokit/app": "^15.0.0", + "@octokit/core": "^6.0.0", + "@octokit/oauth-app": "^7.0.0", + "@octokit/plugin-paginate-graphql": "^5.0.0", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-rest-endpoint-methods": "^13.0.0", + "@octokit/plugin-retry": "^7.0.0", + "@octokit/plugin-throttling": "^9.0.0", + "@octokit/request-error": "^6.0.0", + "@octokit/types": "^13.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "license": "MIT", @@ -9723,11 +9837,12 @@ } }, "node_modules/p-limit": { - "version": "5.0.0", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.1.0.tgz", + "integrity": "sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==", "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" + "yocto-queue": "^1.1.1" }, "engines": { "node": ">=18" @@ -12058,9 +12173,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/universal-github-app-jwt": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.0.tgz", + "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==", + "license": "MIT" + }, "node_modules/universal-user-agent": { "version": "7.0.2", - "dev": true, "license": "ISC" }, "node_modules/universalify": { @@ -12610,7 +12730,6 @@ }, "node_modules/yocto-queue": { "version": "1.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=12.20" diff --git a/package.json b/package.json index 3570dfd..4804ea1 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/diff": "^5.2.1", "@types/eslint__js": "^8.42.3", "@types/express": "^4.17.17", + "@types/mustache": "^4.2.5", "@types/node": "^18.7.13", "@types/prompts": "2.4.9", "@types/validate-npm-package-name": "4.0.2", @@ -63,8 +64,11 @@ "crypto-random-string": "^5.0.0", "diff": "^5.2.0", "inquirer": "^9.2.6", + "mustache": "^4.2.0", + "octokit": "^4.0.2", "open": "^10.1.0", "openid-client": "^5.6.5", + "p-limit": "^6.1.0", "prompts": "2.4.2", "validate-npm-package-name": "5.0.1", "which": "^3.0.1", diff --git a/src/commands/__snapshots__/scaffold.test.ts.snap b/src/commands/__snapshots__/scaffold.test.ts.snap new file mode 100644 index 0000000..c939e87 --- /dev/null +++ b/src/commands/__snapshots__/scaffold.test.ts.snap @@ -0,0 +1,13 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`scaffold > list 1`] = ` +"- name: astro +- name: laravel-11 +" +`; + +exports[`scaffold > start validate template is set 1`] = `""`; + +exports[`scaffold > start with project id 1`] = `""`; + +exports[`scaffold > start without project id 1`] = `""`; diff --git a/src/commands/index.ts b/src/commands/index.ts index 3b0ec5d..6ca11f1 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -3,6 +3,7 @@ import * as projects from './projects.js'; import * as ipAllow from './ip_allow.js'; import * as users from './user.js'; import * as orgs from './orgs.js'; +import * as scaffold from './scaffold.js'; import * as branches from './branches.js'; import * as databases from './databases.js'; import * as roles from './roles.js'; @@ -15,6 +16,7 @@ export default [ auth, users, orgs, + scaffold, projects, ipAllow, branches, diff --git a/src/commands/projects.ts b/src/commands/projects.ts index f21b7ca..b2c09ec 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -194,7 +194,7 @@ const list = async (props: CommonProps & { orgId?: string }) => { out.end(); }; -const create = async ( +export const create = async ( props: CommonProps & { name?: string; regionId?: string; @@ -252,6 +252,8 @@ const create = async ( const psqlArgs = props['--']; await psql(connection_uri, psqlArgs); } + + return data; }; const deleteProject = async (props: CommonProps & IdOrNameProps) => { diff --git a/src/commands/scaffold.test.ts b/src/commands/scaffold.test.ts new file mode 100644 index 0000000..c5aa88d --- /dev/null +++ b/src/commands/scaffold.test.ts @@ -0,0 +1,20 @@ +import { describe } from 'vitest'; + +import { test } from '../test_utils/fixtures'; + +describe('scaffold', () => { + test('list', async ({ testCliCommand }) => { + await testCliCommand(['scaffold', 'list']); + }); + + test('start with project id', async ({ testCliCommand }) => { + await testCliCommand([ + 'scaffold', + 'start', + '--template-id', + 'test-template', + '--project-id', + 'test', + ]); + }); +}); diff --git a/src/commands/scaffold.ts b/src/commands/scaffold.ts new file mode 100644 index 0000000..3f43ee1 --- /dev/null +++ b/src/commands/scaffold.ts @@ -0,0 +1,346 @@ +import yargs from 'yargs'; + +import { CommonProps, IdOrNameProps } from '../types.js'; +import { writer } from '../writer.js'; +import { log } from '../log.js'; +import path from 'path'; +import fs from 'fs'; +import prompts from 'prompts'; + +import Mustache from 'mustache'; + +import { projectCreateRequest } from '../parameters.gen.js'; + +import { create as createProject } from '../commands/projects.js'; + +import { + downloadFolderFromTree, + getContent, + getFileContent, +} from '../utils/github.js'; +import { Branch, Database, Role } from '@neondatabase/api-client'; + +const TEMPLATE_LIST_FIELDS = ['name'] as const; + +const REPOSITORY_OWNER = 'neon-scaffolder'; +const REPOSITORY = 'templates'; + +const GENERATED_FOLDER_PREFIX = 'neon-'; + +const REGIONS = [ + 'aws-us-west-2', + 'aws-ap-southeast-1', + 'aws-eu-central-1', + 'aws-us-east-2', + 'aws-us-east-1', +]; + +export const command = 'scaffold'; +export const describe = 'Create new project from selected template'; +export const aliases = ['scaffold']; +export const builder = (argv: yargs.Argv) => { + return argv + .usage('$0 scaffold [options]') + .command( + 'list', + 'List available templates', + (yargs) => yargs, + async (args) => { + await list(args); + }, + ) + .command( + 'start', + 'Create new project from selected template', + (yargs) => + yargs.options({ + 'project-id': { + describe: + 'ID of existing project. If not set, new project will be created', + type: 'string', + }, + 'template-id': { + describe: 'ID (name) of the template', + type: 'string', + }, + name: { + describe: projectCreateRequest['project.name'].description, + type: 'string', + }, + 'output-dir': { + describe: 'Output directory', + type: 'string', + }, + 'region-id': { + describe: `The region ID. Possible values: ${REGIONS.join(', ')}`, + type: 'string', + }, + 'org-id': { + describe: "The project's organization ID", + type: 'string', + }, + psql: { + type: 'boolean', + describe: 'Connect to a new project via psql', + default: false, + }, + database: { + describe: + projectCreateRequest['project.branch.database_name'].description, + type: 'string', + }, + role: { + describe: + projectCreateRequest['project.branch.role_name'].description, + type: 'string', + }, + 'set-context': { + type: 'boolean', + describe: 'Set the current context to the new project', + default: false, + }, + cu: { + describe: + 'The number of Compute Units. Could be a fixed size (e.g. "2") or a range delimited by a dash (e.g. "0.5-3").', + type: 'string', + }, + }), + async (args) => { + // @ts-expect-error: TODO - Assert `args` is `CommonProps` + await start(args); + }, + ); +}; +export const handler = (args: yargs.Argv) => { + return args; +}; + +async function getTemplateList() { + const content = await getContent({ + owner: REPOSITORY_OWNER, + repository: REPOSITORY, + }); + + if (!content) { + log.error('No templates found.'); + return []; + } + if (!Array.isArray(content)) { + log.error('Invalid content received from GitHub API.'); + return []; + } + return content + .filter((el: any) => el.type === 'dir') + .map((el: any) => ({ name: el.name })); +} + +const list = async (props: CommonProps) => { + const out = writer(props); + + out.write(await getTemplateList(), { + fields: TEMPLATE_LIST_FIELDS, + title: 'Templates', + }); + out.end(); +}; + +const start = async ( + props: CommonProps & + IdOrNameProps & { + templateId?: string; + projectId?: string; + outputDir?: string; + name?: string; + regionId?: string; + cu?: string; + orgId?: string; + database?: string; + role?: string; + psql: boolean; + setContext: boolean; + '--'?: string[]; + }, +) => { + const availableTemplates = (await getTemplateList()).map( + (el: any) => el.name, + ); + + let projectData: { + project: { + id: string; + name: string; + }; + connection_uris?: { + connection_uri: string; + connection_parameters: { + database: string; + role: string; + password: string; + host: string; + }; + }[]; + } = { + project: { + id: '', + name: '', + }, + connection_uris: [], + }; + + if (!props.projectId) { + const userProjects = await props.apiClient.listProjects({ + org_id: props.orgId, + search: props.name, + limit: 10, + }); + const selectedProject = await prompts({ + type: 'select', + name: 'value', + message: 'Select your Neon project', + choices: [ + { title: 'New', description: 'Create new project', value: false }, + ...userProjects.data.projects.map((project: any) => ({ + title: project.name, + description: project.id, + value: project.id, + })), + ], + initial: false, + }); + + let projectId = selectedProject.value; + if (selectedProject.value === false) { + projectData = await createProject(props); + projectId = projectData.project.id; + } else { + projectData = (await props.apiClient.getProject(projectId)).data; + const branches: Branch[] = ( + await props.apiClient.listProjectBranches(projectId) + ).data.branches; + const roles: Role[] = ( + await props.apiClient.listProjectBranchRoles(projectId, branches[0].id) + ).data.roles; + + const databases: Database[] = ( + await props.apiClient.listProjectBranchDatabases( + projectId, + branches[0].id, + ) + ).data.databases; + + const connectionString: string = ( + await props.apiClient.getConnectionUri({ + projectId: projectId, + database_name: databases[0].name, + role_name: roles[0].name, + }) + ).data.uri; + const connectionUrl = new URL(connectionString); + + projectData.connection_uris = [ + { + connection_uri: connectionString, + connection_parameters: { + database: connectionUrl.pathname.slice(1), + role: connectionUrl.username, + password: connectionUrl.password, + host: connectionUrl.host, + }, + }, + ]; + } + } + + if (!props.templateId) { + const selectedTemplate = await prompts({ + type: 'select', + name: 'value', + message: 'Select your Neon template', + choices: availableTemplates.map((template: any) => ({ + title: template, + value: template, + })), + }); + + props.templateId = selectedTemplate.value; + } + + if (!availableTemplates.includes(props.templateId)) { + log.error( + `Template ${props.templateId} not found. Please make sure the template exists and is public.`, + ); + return; + } + + let config; + try { + config = await ( + await getFileContent( + REPOSITORY_OWNER, + REPOSITORY, + (props.templateId as string) + '/config.neon.json', + ) + ).json(); + } catch (e) { + log.error( + "Couldn't fetch template config file. Please make sure the template exists and is public.", + ); + log.error(e); + return; + } + + const dir = path.join( + process.cwd(), + props.outputDir + ? props.outputDir + : GENERATED_FOLDER_PREFIX + projectData.project.name, + ); + await downloadFolderFromTree( + REPOSITORY_OWNER, + REPOSITORY, + 'main', + props.templateId as string, + dir, + ); + + for (const [key, value] of Object.entries(config.copy_files)) { + copyFiles(dir, key, value as string[]); + } + + for (const file of config.templated_files) { + const content = fs.readFileSync(path.join(dir, file), { + encoding: 'utf-8', + }); + + const output = Mustache.render(content, projectData); + fs.writeFileSync(path.join(dir, file), output); + } + + deleteFiles(dir, config.delete_files); + + const out = writer(props); + + out.write( + [{ name: projectData.project.name, template: props.templateId, path: dir }], + { + fields: ['name', 'template', 'path'], + title: 'Created projects', + }, + ); + out.end(); +}; + +function copyFiles(prefix: string, sourceFile: string, targetFiles: string[]) { + for (const targetFile of targetFiles) { + const sourcePath = path.join(prefix, sourceFile); + const targetPath = path.join(prefix, targetFile); + fs.copyFileSync(sourcePath, targetPath); + } +} + +function deleteFiles(prefix: string, files: string[]) { + for (const file of files) { + const filePath = path.join(prefix, file); + fs.unlinkSync(filePath); + } +} diff --git a/src/utils/github.ts b/src/utils/github.ts new file mode 100644 index 0000000..fa2e242 --- /dev/null +++ b/src/utils/github.ts @@ -0,0 +1,143 @@ +import { Octokit } from 'octokit'; +import axios from 'axios'; +import pLimit from 'p-limit'; + +import { log } from '../log.js'; + +import path from 'path'; +import fs from 'fs'; + +const CONCURRENT_OPERATIONS_LIMIT = 10; + +export async function getContent({ + owner, + repository, +}: { + owner: string; + repository: string; +}) { + const octokit = new Octokit({}); + return ( + await octokit.rest.repos.getContent({ + owner: owner, + repo: repository, + }) + ).data; +} + +export function getFileContentUrl( + owner: string, + repository: string, + path: string, +) { + return ( + 'https://raw.githubusercontent.com/' + + owner + + '/' + + repository + + '/main/' + + path + ); +} + +export async function getFileContent( + owner: string, + repository: string, + path: string, +) { + const url = getFileContentUrl(owner, repository, path); + + return await fetch(url, { method: 'Get' }); +} + +export async function getBranchSHA( + owner: string, + repo: string, + branch: string, +) { + const branchUrl = `https://api.github.com/repos/${owner}/${repo}/branches/${branch}`; + const response = await axios.get(branchUrl, { + headers: { Accept: 'application/vnd.github.v3+json' }, + }); + return response.data.commit.sha; +} + +export async function getRepoTree(owner: string, repo: string, sha: string) { + const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}?recursive=1`; + const response = await axios.get(treeUrl, { + headers: { Accept: 'application/vnd.github.v3+json' }, + }); + return response.data.tree; +} + +export async function downloadFolderFromTree( + owner: string, + repo: string, + branch: string, + folderPath: string, + destination: string, +) { + try { + const sha = await getBranchSHA(owner, repo, branch); + + const tree = await getRepoTree(owner, repo, sha); + + const folderTree = tree.filter( + (item: any) => item.path.startsWith(folderPath) && item.type === 'blob', + ); + + for (const file of folderTree) { + const savePath = path.join( + destination, + file.path.replace(folderPath, ''), + ); + const fileDir = path.dirname(savePath); + if (!fs.existsSync(fileDir)) { + fs.mkdirSync(fileDir, { recursive: true }); + } + } + + const limit = pLimit(CONCURRENT_OPERATIONS_LIMIT); + + const downloadPromises = folderTree.map((file: any) => { + const filePath = file.path; + const savePath = path.join(destination, filePath.replace(folderPath, '')); + const fileUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`; + + return limit(() => + downloadFile(fileUrl, savePath) + .then(() => { + log.debug(`Downloaded ${filePath}`); + }) + .catch((err: unknown) => { + let errorMessage = `Error downloading ${filePath}`; + if (err instanceof Error) { + errorMessage += ': ' + err.message; + } + log.error(errorMessage); + }), + ); + }); + + await Promise.all(downloadPromises); + + log.info('All files downloaded successfully.'); + } catch (error: any) { + log.error('Error downloading folder:', error.message); + } +} + +export async function downloadFile(url: string, savePath: string) { + const response = await axios({ + url, + method: 'GET', + responseType: 'stream', + }); + const writer = fs.createWriteStream(savePath); + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); +}