From 44e161c373d9a3dd121ce91e102ab00ff382b066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ko=C5=82odziejczak?= <31549762+mrl5@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:36:10 +0200 Subject: [PATCH 1/6] fix: return correct neonctl version (#271) closes #196 --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 411f21d..ce86809 100644 --- a/src/index.ts +++ b/src/index.ts @@ -146,6 +146,7 @@ builder = builder default: true, }) .middleware(analyticsMiddleware, true) + .version(pkg.version) .group('version', 'Global options:') .alias('version', 'v') .completion() From 26cccea10264da0d36e693137c1e7a037c0fcd4e Mon Sep 17 00:00:00 2001 From: Timotej Avsec Date: Thu, 19 Sep 2024 17:43:34 +0200 Subject: [PATCH 2/6] feat(scaffolder): add scaffolder command --- package-lock.json | 438 +++++++++++------- package.json | 5 + .../__snapshots__/scaffold.test.ts.snap | 13 + src/commands/index.ts | 2 + src/commands/projects.ts | 4 +- src/commands/scaffold.test.ts | 29 ++ src/commands/scaffold.ts | 257 ++++++++++ src/utils/github.ts | 127 +++++ 8 files changed, 715 insertions(+), 160 deletions(-) create mode 100644 src/commands/__snapshots__/scaffold.test.ts.snap create mode 100644 src/commands/scaffold.test.ts create mode 100644 src/commands/scaffold.ts create mode 100644 src/utils/github.ts diff --git a/package-lock.json b/package-lock.json index 9889fd0..835eee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,14 +13,17 @@ "@segment/analytics-node": "^1.0.0-beta.26", "axios": "^1.4.0", "axios-debug-log": "^1.0.0", - "bun": "^1.1.21", + "buffer": "^6.0.3", "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 +49,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 +976,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 +1104,6 @@ }, "node_modules/@octokit/endpoint": { "version": "10.1.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^13.0.0", @@ -1011,7 +1115,6 @@ }, "node_modules/@octokit/graphql": { "version": "8.1.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/request": "^9.0.0", @@ -1022,14 +1125,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 +1220,6 @@ }, "node_modules/@octokit/plugin-retry": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/request-error": "^6.0.0", @@ -1059,7 +1235,6 @@ }, "node_modules/@octokit/plugin-throttling": { "version": "9.3.1", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^13.0.0", @@ -1074,7 +1249,6 @@ }, "node_modules/@octokit/request": { "version": "9.1.3", - "dev": true, "license": "MIT", "dependencies": { "@octokit/endpoint": "^10.0.0", @@ -1088,7 +1262,6 @@ }, "node_modules/@octokit/request-error": { "version": "6.1.4", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^13.0.0" @@ -1099,115 +1272,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 +2019,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 +2159,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 +2510,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 +2842,6 @@ }, "node_modules/before-after-hook": { "version": "3.0.2", - "dev": true, "license": "Apache-2.0" }, "node_modules/bl": { @@ -2794,7 +2913,6 @@ }, "node_modules/bottleneck": { "version": "2.19.5", - "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -2819,6 +2937,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 +2970,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 +6860,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 +7699,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 +7985,7 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.4.5", + "version": "10.4.2", "dev": true, "inBundle": true, "license": "ISC", @@ -7900,6 +8000,9 @@ "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -8358,15 +8461,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 +9613,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 +9838,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 +12174,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 +12731,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 3f2bf8a..d839a38 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", @@ -58,13 +59,17 @@ "@segment/analytics-node": "^1.0.0-beta.26", "axios": "^1.4.0", "axios-debug-log": "^1.0.0", + "buffer": "^6.0.3", "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", 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 807eef3..5c68c70 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -208,7 +208,7 @@ const list = async (props: CommonProps & { orgId?: string }) => { out.end(); }; -const create = async ( +export const create = async ( props: CommonProps & { name?: string; regionId?: string; @@ -266,6 +266,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..2fdc83f --- /dev/null +++ b/src/commands/scaffold.test.ts @@ -0,0 +1,29 @@ +import { describe } from 'vitest'; + +import { test } from '../test_utils/fixtures'; + +describe('scaffold', () => { + test('list', async ({ testCliCommand }) => { + await testCliCommand(['scaffold', 'list']); + }); + + test('start validate template is set', async ({ testCliCommand }) => { + await testCliCommand(['scaffold', 'start'], { + code: 1, + }); + }); + + test('start with project id', async ({ testCliCommand }) => { + await testCliCommand([ + 'scaffold', + 'start', + 'test-template', + '--project-id', + 'test', + ]); + }); + + test('start without project id', async ({ testCliCommand }) => { + await testCliCommand(['scaffold', 'start', 'test-template']); + }); +}); diff --git a/src/commands/scaffold.ts b/src/commands/scaffold.ts new file mode 100644 index 0000000..158ef72 --- /dev/null +++ b/src/commands/scaffold.ts @@ -0,0 +1,257 @@ +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 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'; + +const TEMPLATE_LIST_FIELDS = ['name'] as const; + +const REPOSITORY_OWNER = 'neon-scaffolder'; +const REPOSITORY = 'templates'; + +const GENERATED_FOLDER_PREFIX = 'neon-'; + +// TODO: Maybe move to constants file? +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) => { + // @ts-expect-error: TODO - Assert `args` is `CommonProps` + 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', + }, + 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(REPOSITORY_OWNER, REPOSITORY); + + 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 & { + id: 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, + ); + if (!availableTemplates.includes(props.id)) { + log.error( + 'Template not found. Please make sure the template exists and is public.', + ); + return; + } + let projectData: any; + if (!props.projectId) { + projectData = await createProject(props); + } else { + projectData = (await props.apiClient.getProject(props.projectId)).data; + const branches: any = ( + await props.apiClient.listProjectBranches(props.projectId) + ).data.branches; + const roles: any = ( + await props.apiClient.listProjectBranchRoles( + props.projectId, + branches[0].id, + ) + ).data.roles; + + const connectionString: any = ( + await props.apiClient.getConnectionUri({ + projectId: props.projectId, + database_name: branches[0].name, + role_name: roles[0].name, + }) + ).data.uri; + const connectionUrl = new URL(connectionString); + + projectData.connection_uris = [ + { + connection_uri: connectionString, + connection_parameters: { + database: projectData.project.name, + role: roles[0].name, + password: connectionUrl.password, + host: connectionUrl.host, + }, + }, + ]; + } + + let config = null; + try { + config = await ( + await getFileContent( + REPOSITORY_OWNER, + REPOSITORY, + props.id + '/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 as string), + ); + await downloadFolderFromTree( + REPOSITORY_OWNER, + REPOSITORY, + 'main', + props.id, + dir, + ); + + for (const [key, value] of Object.entries(config.copy_files)) { + copyFiles(dir, key, value); + } + + 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); + } + + const out = writer(props); + + out.write( + [{ name: projectData.project.name, template: props.id, path: dir }], + { + fields: ['name', 'template', 'path'], + title: 'Created projects', + }, + ); + out.end(); +}; + +function copyFiles(prefix: string, sourceFile: string, targetFiles: any) { + for (const targetFile of targetFiles) { + const sourcePath = path.join(prefix, sourceFile); + const targetPath = path.join(prefix, targetFile); + fs.copyFileSync(sourcePath, targetPath); + } +} diff --git a/src/utils/github.ts b/src/utils/github.ts new file mode 100644 index 0000000..e9086a9 --- /dev/null +++ b/src/utils/github.ts @@ -0,0 +1,127 @@ +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 MULTITHREADING_LIMIT = 10; + +export async function getContent(owner: string, repository: string) { + const octokit = new Octokit({}); + return ( + await octokit.rest.repos.getContent({ + owner: owner, + repo: repository, + }) + ).data; +} + +export async function getFileContent( + owner: string, + repository: string, + path: string, +) { + const url = + 'https://raw.githubusercontent.com/' + + owner + + '/' + + repository + + '/main/' + + 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(MULTITHREADING_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); + }); +} From 51fe65f6d0cc02096f3829127faa0386ef02f7b8 Mon Sep 17 00:00:00 2001 From: Fedor Dikarev Date: Fri, 11 Oct 2024 13:35:36 +0200 Subject: [PATCH 3/6] ci: gh-workflow-stats-action: bump version and move action repo (#297) Move action repo from personal account to org, and bump to last version. --- .github/workflows/report-workflow-stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/report-workflow-stats.yml b/.github/workflows/report-workflow-stats.yml index 45b6132..dbfd965 100644 --- a/.github/workflows/report-workflow-stats.yml +++ b/.github/workflows/report-workflow-stats.yml @@ -16,7 +16,7 @@ jobs: actions: read steps: - name: Export GH Workflow Stats - uses: fedordikarev/gh-workflow-stats-action@v0.1.3 + uses: neondatabase/gh-workflow-stats-action@v0.1.4 with: DB_URI: ${{ secrets.GH_REPORT_STATS_DB_RW_CONNSTR }} DB_TABLE: 'gh_workflow_stats_neonctl' From 9b3c0435d82380e4d1df82f3be6833d75498dd60 Mon Sep 17 00:00:00 2001 From: Timotej Avsec Date: Thu, 17 Oct 2024 08:00:52 +0200 Subject: [PATCH 4/6] chore(lint): fix lint --- src/commands/scaffold.test.ts | 11 +-- src/commands/scaffold.ts | 151 +++++++++++++++++++++++----------- 2 files changed, 104 insertions(+), 58 deletions(-) diff --git a/src/commands/scaffold.test.ts b/src/commands/scaffold.test.ts index 2fdc83f..c5aa88d 100644 --- a/src/commands/scaffold.test.ts +++ b/src/commands/scaffold.test.ts @@ -7,23 +7,14 @@ describe('scaffold', () => { await testCliCommand(['scaffold', 'list']); }); - test('start validate template is set', async ({ testCliCommand }) => { - await testCliCommand(['scaffold', 'start'], { - code: 1, - }); - }); - test('start with project id', async ({ testCliCommand }) => { await testCliCommand([ 'scaffold', 'start', + '--template-id', 'test-template', '--project-id', 'test', ]); }); - - test('start without project id', async ({ testCliCommand }) => { - await testCliCommand(['scaffold', 'start', 'test-template']); - }); }); diff --git a/src/commands/scaffold.ts b/src/commands/scaffold.ts index 158ef72..90efb63 100644 --- a/src/commands/scaffold.ts +++ b/src/commands/scaffold.ts @@ -5,6 +5,7 @@ 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'; @@ -50,7 +51,7 @@ export const builder = (argv: yargs.Argv) => { }, ) .command( - 'start ', + 'start', 'Create new project from selected template', (yargs) => yargs.options({ @@ -59,6 +60,10 @@ export const builder = (argv: yargs.Argv) => { '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', @@ -132,7 +137,7 @@ const list = async (props: CommonProps) => { const start = async ( props: CommonProps & IdOrNameProps & { - id: string; + templateId?: string; projectId?: string; outputDir?: string; name?: string; @@ -149,48 +154,84 @@ const start = async ( const availableTemplates = (await getTemplateList()).map( (el: any) => el.name, ); - if (!availableTemplates.includes(props.id)) { + + let projectData: any; + + if (!props.projectId) { + const userProjects = await props.apiClient.listProjects({ + 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: any = ( + await props.apiClient.listProjectBranches(projectId) + ).data.branches; + const roles: any = ( + await props.apiClient.listProjectBranchRoles(projectId, branches[0].id) + ).data.roles; + + const connectionString: any = ( + await props.apiClient.getConnectionUri({ + projectId: projectId, + database_name: branches[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 not found. Please make sure the template exists and is public.', ); return; } - let projectData: any; - if (!props.projectId) { - projectData = await createProject(props); - } else { - projectData = (await props.apiClient.getProject(props.projectId)).data; - const branches: any = ( - await props.apiClient.listProjectBranches(props.projectId) - ).data.branches; - const roles: any = ( - await props.apiClient.listProjectBranchRoles( - props.projectId, - branches[0].id, - ) - ).data.roles; - - const connectionString: any = ( - await props.apiClient.getConnectionUri({ - projectId: props.projectId, - database_name: branches[0].name, - role_name: roles[0].name, - }) - ).data.uri; - const connectionUrl = new URL(connectionString); - - projectData.connection_uris = [ - { - connection_uri: connectionString, - connection_parameters: { - database: projectData.project.name, - role: roles[0].name, - password: connectionUrl.password, - host: connectionUrl.host, - }, - }, - ]; - } let config = null; try { @@ -198,7 +239,7 @@ const start = async ( await getFileContent( REPOSITORY_OWNER, REPOSITORY, - props.id + '/config.neon.json', + (props.templateId as string) + '/config.neon.json', ) ).json(); } catch (e) { @@ -215,13 +256,18 @@ const start = async ( ? props.outputDir : GENERATED_FOLDER_PREFIX + (projectData.project.name as string), ); - await downloadFolderFromTree( - REPOSITORY_OWNER, - REPOSITORY, - 'main', - props.id, - dir, - ); + if (props.templateId) { + await downloadFolderFromTree( + REPOSITORY_OWNER, + REPOSITORY, + 'main', + props.templateId, + dir, + ); + } else { + log.error('Template ID is undefined.'); + return; + } for (const [key, value] of Object.entries(config.copy_files)) { copyFiles(dir, key, value); @@ -236,10 +282,12 @@ const start = async ( fs.writeFileSync(path.join(dir, file), output); } + deleteFiles(dir, config.delete_files); + const out = writer(props); out.write( - [{ name: projectData.project.name, template: props.id, path: dir }], + [{ name: projectData.project.name, template: props.templateId, path: dir }], { fields: ['name', 'template', 'path'], title: 'Created projects', @@ -255,3 +303,10 @@ function copyFiles(prefix: string, sourceFile: string, targetFiles: any) { fs.copyFileSync(sourcePath, targetPath); } } + +function deleteFiles(prefix: string, files: any) { + for (const file of files) { + const filePath = path.join(prefix, file); + fs.unlinkSync(filePath); + } +} From d67f019de40aa085fad99c609a2fe4bc79e38e00 Mon Sep 17 00:00:00 2001 From: Timotej Avsec Date: Sat, 9 Nov 2024 19:03:11 +0100 Subject: [PATCH 5/6] chore: fix PR comments --- package-lock.json | 1 - package.json | 1 - src/commands/scaffold.ts | 62 ++++++++++++++++++++++++---------------- src/utils/github.ts | 30 ++++++++++++++----- 4 files changed, 61 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2be70ef..f2e4421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@segment/analytics-node": "^1.0.0-beta.26", "axios": "^1.4.0", "axios-debug-log": "^1.0.0", - "buffer": "^6.0.3", "chalk": "^5.2.0", "cli-table": "^0.3.11", "crypto-random-string": "^5.0.0", diff --git a/package.json b/package.json index 5727ebe..1e59e70 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@segment/analytics-node": "^1.0.0-beta.26", "axios": "^1.4.0", "axios-debug-log": "^1.0.0", - "buffer": "^6.0.3", "chalk": "^5.2.0", "cli-table": "^0.3.11", "crypto-random-string": "^5.0.0", diff --git a/src/commands/scaffold.ts b/src/commands/scaffold.ts index 90efb63..a98760e 100644 --- a/src/commands/scaffold.ts +++ b/src/commands/scaffold.ts @@ -18,6 +18,7 @@ import { getContent, getFileContent, } from '../utils/github.js'; +import { Branch, Database, Role } from '@neondatabase/api-client'; const TEMPLATE_LIST_FIELDS = ['name'] as const; @@ -26,7 +27,6 @@ const REPOSITORY = 'templates'; const GENERATED_FOLDER_PREFIX = 'neon-'; -// TODO: Maybe move to constants file? const REGIONS = [ 'aws-us-west-2', 'aws-ap-southeast-1', @@ -38,7 +38,7 @@ const REGIONS = [ export const command = 'scaffold'; export const describe = 'Create new project from selected template'; export const aliases = ['scaffold']; -export const builder = (argv: yargs.Argv) => { +export const builder = (argv: yargs.Argv) => { return argv .usage('$0 scaffold [options]') .command( @@ -46,7 +46,6 @@ export const builder = (argv: yargs.Argv) => { 'List available templates', (yargs) => yargs, async (args) => { - // @ts-expect-error: TODO - Assert `args` is `CommonProps` await list(args); }, ) @@ -117,8 +116,19 @@ export const handler = (args: yargs.Argv) => { }; async function getTemplateList() { - const content = await getContent(REPOSITORY_OWNER, REPOSITORY); + 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 })); @@ -159,6 +169,8 @@ const start = async ( if (!props.projectId) { const userProjects = await props.apiClient.listProjects({ + org_id: props.orgId, + search: props.name, limit: 10, }); const selectedProject = await prompts({ @@ -182,17 +194,24 @@ const start = async ( projectId = projectData.project.id; } else { projectData = (await props.apiClient.getProject(projectId)).data; - const branches: any = ( + const branches: Branch[] = ( await props.apiClient.listProjectBranches(projectId) ).data.branches; - const roles: any = ( + const roles: Role[] = ( await props.apiClient.listProjectBranchRoles(projectId, branches[0].id) ).data.roles; - const connectionString: any = ( + const databases: Database[] = ( + await props.apiClient.listProjectBranchDatabases( + projectId, + branches[0].id, + ) + ).data.databases; + + const connectionString: string = ( await props.apiClient.getConnectionUri({ projectId: projectId, - database_name: branches[0].name, + database_name: databases[0].name, role_name: roles[0].name, }) ).data.uri; @@ -228,7 +247,7 @@ const start = async ( if (!availableTemplates.includes(props.templateId)) { log.error( - 'Template not found. Please make sure the template exists and is public.', + `Template ${props.templateId} not found. Please make sure the template exists and is public.`, ); return; } @@ -256,21 +275,16 @@ const start = async ( ? props.outputDir : GENERATED_FOLDER_PREFIX + (projectData.project.name as string), ); - if (props.templateId) { - await downloadFolderFromTree( - REPOSITORY_OWNER, - REPOSITORY, - 'main', - props.templateId, - dir, - ); - } else { - log.error('Template ID is undefined.'); - return; - } + 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); + copyFiles(dir, key, value as string[]); } for (const file of config.templated_files) { @@ -296,7 +310,7 @@ const start = async ( out.end(); }; -function copyFiles(prefix: string, sourceFile: string, targetFiles: any) { +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); @@ -304,7 +318,7 @@ function copyFiles(prefix: string, sourceFile: string, targetFiles: any) { } } -function deleteFiles(prefix: string, files: any) { +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 index e9086a9..fa2e242 100644 --- a/src/utils/github.ts +++ b/src/utils/github.ts @@ -7,9 +7,15 @@ import { log } from '../log.js'; import path from 'path'; import fs from 'fs'; -const MULTITHREADING_LIMIT = 10; - -export async function getContent(owner: string, repository: string) { +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({ @@ -19,18 +25,28 @@ export async function getContent(owner: string, repository: string) { ).data; } -export async function getFileContent( +export function getFileContentUrl( owner: string, repository: string, path: string, ) { - const url = + return ( 'https://raw.githubusercontent.com/' + owner + '/' + repository + '/main/' + - path; + path + ); +} + +export async function getFileContent( + owner: string, + repository: string, + path: string, +) { + const url = getFileContentUrl(owner, repository, path); + return await fetch(url, { method: 'Get' }); } @@ -81,7 +97,7 @@ export async function downloadFolderFromTree( } } - const limit = pLimit(MULTITHREADING_LIMIT); + const limit = pLimit(CONCURRENT_OPERATIONS_LIMIT); const downloadPromises = folderTree.map((file: any) => { const filePath = file.path; From a1ab1cba70e37c9071f6a6a39064510be594f852 Mon Sep 17 00:00:00 2001 From: Timotej Avsec Date: Sat, 9 Nov 2024 19:13:36 +0100 Subject: [PATCH 6/6] chore: fix PR comments --- src/commands/scaffold.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/commands/scaffold.ts b/src/commands/scaffold.ts index a98760e..3f43ee1 100644 --- a/src/commands/scaffold.ts +++ b/src/commands/scaffold.ts @@ -165,7 +165,27 @@ const start = async ( (el: any) => el.name, ); - let projectData: any; + 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({ @@ -252,7 +272,7 @@ const start = async ( return; } - let config = null; + let config; try { config = await ( await getFileContent( @@ -273,7 +293,7 @@ const start = async ( process.cwd(), props.outputDir ? props.outputDir - : GENERATED_FOLDER_PREFIX + (projectData.project.name as string), + : GENERATED_FOLDER_PREFIX + projectData.project.name, ); await downloadFolderFromTree( REPOSITORY_OWNER,