From d4afe0994460c4b04828cc990bb36282f6931eb1 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Wed, 11 Dec 2024 19:54:38 +0100 Subject: [PATCH] build: first commit --- .editorconfig | 22 ++++++ .gitattributes | 1 + .github/dependabot.yml | 11 +++ .github/workflows/main.yml | 69 +++++++++++++++++++ .github/workflows/pull_request.yml | 36 ++++++++++ .gitignore | 33 +++++++++ .npmignore | 16 +++++ .npmrc | 1 + LICENSE.md | 21 ++++++ README.md | 41 ++++++++++++ bin/index.ts | 44 ++++++++++++ package.json | 88 ++++++++++++++++++++++++ src/index.ts | 104 +++++++++++++++++++++++++++++ test/index.test.ts | 26 ++++++++ vitest.config.ts | 7 ++ 15 files changed, 520 insertions(+) create mode 100755 .editorconfig create mode 100755 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pull_request.yml create mode 100755 .gitignore create mode 100755 .npmignore create mode 100644 .npmrc create mode 100755 LICENSE.md create mode 100644 README.md create mode 100644 bin/index.ts create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 test/index.test.ts create mode 100644 vitest.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..c3efa59 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 100 +indent_brace_style = 1TBS +spaces_around_operators = true +quote_type = auto + +[package.json] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b479c92 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: '/' + schedule: + interval: daily + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + # Check for updates to GitHub Actions every weekday + interval: 'daily' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ef8f822 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,69 @@ +name: main + +on: + push: + branches: + - master + +jobs: + contributors: + if: "${{ github.event.head_commit.message != 'build: contributors' }}" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Contributors + run: | + git config --global user.email ${{ secrets.GIT_EMAIL }} + git config --global user.name ${{ secrets.GIT_USERNAME }} + npm run contributors + - name: Push changes + run: | + git push origin ${{ github.head_ref }} + + release: + if: | + !startsWith(github.event.head_commit.message, 'chore(release):') && + !startsWith(github.event.head_commit.message, 'docs:') && + !startsWith(github.event.head_commit.message, 'ci:') + needs: [contributors] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Setup PNPM + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + - name: Test + run: pnpm test + - name: Report + run: npx c8 report --reporter=text-lcov > coverage/lcov.info + # - name: Coverage + # uses: coverallsapp/github-action@main + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Release + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + git config --global user.email ${{ secrets.GIT_EMAIL }} + git config --global user.name ${{ secrets.GIT_USERNAME }} + git pull origin master + pnpm run release diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..268a2b1 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,36 @@ +name: pull_request + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + if: github.ref != 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Setup PNPM + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + - name: Test + run: pnpm test + - name: Report + run: npx c8 report --reporter=text-lcov > coverage/lcov.info + - name: Coverage + uses: coverallsapp/github-action@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..16e7019 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +############################ +# npm +############################ +node_modules +npm-debug.log + +############################ +# tmp, editor & OS files +############################ +.tmp +*.swo +*.swp +*.swn +*.swm +.DS_Store +*# +*~ +.idea +*sublime* +nbproject + +############################ +# Tests +############################ +testApp +coverage +.nyc_output + +############################ +# Other +############################ +.node_history +bin/index.js diff --git a/.npmignore b/.npmignore new file mode 100755 index 0000000..7ec7473 --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +.idea +.project +*.sublime-* +.DS_Store +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.swp +*.swo +node_modules +coverage +*.tgz +*.xml diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4d936e8 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +unsafe-perm=true diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 0000000..518ca31 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2024 Vercel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5531ce1 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# vercel-open + +The `vercel-open` command is an extension for Vercel CLI + +## Installation + +``` +npm install -g vercel-open +``` + +## Usage + +Assuming your terminal is pointing in a local vercel project: + +``` +vc open # open the vercel dahboard for the current project +vc open logs # open logs of the project +vc open logs --timeline=maximum # any query parameter is supported +``` + +Additionally, you can specify the project open: + +``` +vc open vercel/v0 logs # will open `https://vercel.com/vercel/v0/logs` in the browser +``` + +Also `vc open info` will print revelant information about your project: + +``` +> vc open info + +▲ overview https://vercel.com/vercel/v0/ +▲ latest (production) https://vercel.com/vercel/v0/2fVgMqXL3km1nAwqg7aAR58gtt1A/ +▲ latest (preview) https://vercel.com/vercel/v0/UhnKLjQb7SDaLFT2GBM2eVwLUsdE/ +``` + +## License + +**vercel-open** © [Vercel](https://vercel.com), released under the [MIT](https://github.com/microlink/microlink-function/blob/master/LICENSE.md) License.
+ +> [vercel.com](https://vercel.com) · GitHub [vercel](https://github.com/vercel) · X [@vercel](https://x.com/vercel) diff --git a/bin/index.ts b/bin/index.ts new file mode 100644 index 0000000..93a5db8 --- /dev/null +++ b/bin/index.ts @@ -0,0 +1,44 @@ +import openBrowser from 'open' +import mri from 'mri' +import pc from 'picocolors' + +import { + getSlugAndSection, + getLatestDeployment, + getProductionDeployment +} from '../src' + +async function main () { + const { _: args, ...flags } = mri(process.argv.slice(2)) + + const { org, project, section } = await getSlugAndSection({ args }) + + if (section === 'info') { + const latestDeployment = await getLatestDeployment() + const productionDeployment = await getProductionDeployment() + + console.log( + pc.black( + [ + '', + `${pc.white('▲ overview')} https://vercel.com/${org}/${project}/`, + `${pc.white('▲ latest (production)')} https://vercel.com/${org}/${project}/${latestDeployment}/`, + `${pc.white('▲ latest (preview)')} https://vercel.com/${org}/${project}/${productionDeployment}/` + ].join('\n') + ) + ) + } else { + const url = new URL(`${org}/${project}/${section}`, 'https://vercel.com/') + url.search = new URLSearchParams(flags).toString() + + await openBrowser(url.toString()) + } +} + +main() + .then(() => { + process.exit(0) + }) + .catch(err => { + console.error(err) + }) diff --git a/package.json b/package.json new file mode 100644 index 0000000..dcf3784 --- /dev/null +++ b/package.json @@ -0,0 +1,88 @@ +{ + "name": "vercel-open", + "description": "opens a Vercel project in your browser from your terminal", + "homepage": "https://github.com/vercel-labs/vercel-open#readme", + "version": "1.0.0", + "bin": { + "vercel-open": "bin/index.js" + }, + "contributors": [ + { + "name": "Kiko Beats", + "email": "josefrancisco.verdu@gmail.com" + } + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/vercel-open.git" + }, + "bugs": { + "url": "https://github.com/vercel-labs/vercel-open/issues" + }, + "keywords": [ + "cli", + "open", + "vercel" + ], + "dependencies": { + "arg": "~5.0.2", + "mri": "~1.2.0", + "open": "~10.1.0", + "picocolors": "~1.1.1", + "typescript": "~5.7.2" + }, + "devDependencies": { + "@commitlint/cli": "latest", + "@commitlint/config-conventional": "latest", + "@ksmithut/prettier-standard": "latest", + "c8": "latest", + "ci-publish": "latest", + "finepack": "latest", + "git-authors-cli": "latest", + "github-generate-release": "latest", + "nano-staged": "latest", + "simple-git-hooks": "latest", + "standard": "latest", + "standard-markdown": "latest", + "standard-version": "latest", + "tsup": "latest", + "vitest": "latest" + }, + "files": [ + "bin/index.js" + ], + "scripts": { + "build": "tsup --format esm bin/index.ts -d bin/", + "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true", + "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)", + "prepublishOnly": "npm run build", + "release": "standard-version -a", + "release:github": "github-generate-release", + "release:tags": "git push --follow-tags origin HEAD:master", + "test": "c8 vitest" + }, + "preferGlobal": true, + "license": "MIT", + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "nano-staged": { + "*.js": [ + "prettier-standard", + "standard --fix" + ], + "*.md": [ + "standard-markdown" + ], + "package.json": [ + "finepack" + ] + }, + "simple-git-hooks": { + "commit-msg": "npx commitlint --edit", + "pre-commit": "npx nano-staged" + }, + "type": "module" +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8fd4196 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,104 @@ +import { readFile } from 'fs/promises' +import { existsSync } from 'fs' +import path from 'path' + +const vercelApi = pathname => + fetch(`${process.env.VERCEL_API}/${pathname}`, { + headers: { + 'Accept-Encoding': 'identity' + } + }) + +const getProjectName = async (projectId: string) => + vercelApi(`v9/projects/${projectId}`) + .then(res => res.json()) + .then(payload => payload.name) + +const getOrganizationName = async (teamId: string) => + vercelApi(`v2/teams/${teamId}`) + .then(res => res.json()) + .then(payload => payload.name) + +export const getLatestDeployment = async () => { + const { projectId } = await readProjectFile() + return vercelApi(`v9/projects/${projectId}`) + .then(res => res.json()) + .then(payload => payload.latestDeployments[0].id.replace('dpl_', '')) +} + +export const getProductionDeployment = async () => { + const { projectId } = await readProjectFile() + return vercelApi(`v9/projects/${projectId}`) + .then(res => res.json()) + .then(payload => payload.targets.production.id.replace('dpl_', '')) +} + +async function readProjectFile (): Promise<{ + projectId: string + teamId: string +}> { + const filepath = path.resolve(process.cwd(), '.vercel/project.json') + + if (!existsSync(filepath)) { + const error: NodeJS.ErrnoException = new Error('Link project error') + error.code = 'ERR_LINK_PROJECT' + throw error + } + + const fileContent = await readFile(filepath, 'utf-8') + const { projectId, orgId: teamId } = JSON.parse(fileContent) + return { projectId, teamId } +} + +async function fromPath (): Promise<{ org: string; project: string }> { + const { projectId, teamId } = await readProjectFile() + const [org, project] = await Promise.all([ + getOrganizationName(teamId), + getProjectName(projectId) + ]) + + return { org, project } +} + +export async function getSlugAndSection ({ + args = [] +}: { + args?: string[] + cwd?: string +} = {}): Promise<{ + org: string + project: string + section: string +}> { + if (args.length === 0) { + return { ...(await fromPath()), section: '' } + } + + if (args.length === 1) { + if (!args[0].includes('/')) { + return { + ...(await fromPath()), + section: args[0] + } + } else { + const [org, project] = args[0].split('/') + return { + org, + project, + section: '' + } + } + } + + if (args.length === 2) { + const [org, project] = args[0].split('/') + + return { + org, + project, + section: args[1] + } + } + + throw new Error('Invalid arguments') +} diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..00d66f9 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,26 @@ +import { describe, it } from 'vitest' + +import { getSlugAndSection } from '../src' +import { expect } from 'vitest' + +describe('.getSlugAndSection', () => { + it('from arguments', async () => { + const { org, project, section } = await getSlugAndSection({ + args: ['kikobeats/counting'] + }) + + expect(org).toBe('kikobeats') + expect(project).toBe('counting') + expect(section).toBe('') + }) + + it('from arguments with section', async () => { + const { org, project, section } = await getSlugAndSection({ + args: ['kikobeats/counting', 'logs'] + }) + + expect(org).toBe('kikobeats') + expect(project).toBe('counting') + expect(section).toBe('logs') + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1b4265a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.ts'] + } +})