From 0cfb79da46211aa3c4273b3d6a111125e6b716bd Mon Sep 17 00:00:00 2001 From: Nebula Date: Fri, 3 Mar 2023 21:44:32 -0500 Subject: [PATCH] Migrate to CLI v3 (#304) * delete everything * migrate to v3 * basic logout command * remove pre-release text * additional error handling in rlwy domain * fixed backwards logic on rlwy domain * accept environment id in rlwy run * default environment on rlwy link if there's only 1 * dont print plugins and services if there are none * skip serializing nones * remove entities file * rename ServiceDomains * link to project upon init * remove description prompt * fully remove description prompt * remove dead code * refactor some of the commands * update repo target * fix broken render_config * cleanup unused vars * remove muts * bump cargo version to 3.0.0 * change repo on Cargo.toml --------- Co-authored-by: Angelo --- .github/changelog-configuration.json | 11 + .github/workflows/build.yml | 43 - .github/workflows/ci.yml | 88 + .github/workflows/label-check.yml | 4 +- .github/workflows/publish.yml | 90 - .github/workflows/release.yml | 154 + .github/workflows/tag.yml | 58 - .gitignore | 148 +- .goreleaser.yml | 35 - CONTRIBUTING.md | 20 +- Cargo.lock | 2519 +++++++++++++++++ Cargo.toml | 75 + LICENSE | 4 +- Makefile | 5 - README.md | 60 +- bin/railway.js | 17 - cmd/add.go | 39 - cmd/build.go | 24 - cmd/completion.go | 34 - cmd/connect.go | 162 -- cmd/delete.go | 113 - cmd/design.go | 67 - cmd/docs.go | 12 - cmd/down.go | 68 - cmd/environment.go | 62 - cmd/init.go | 250 -- cmd/link.go | 79 - cmd/list.go | 30 - cmd/login.go | 25 - cmd/logout.go | 11 - cmd/logs.go | 15 - cmd/main.go | 18 - cmd/open.go | 47 - cmd/panic.go | 30 - cmd/protect.go | 31 - cmd/run.go | 301 -- cmd/shell.go | 60 - cmd/status.go | 57 - cmd/unlink.go | 34 - cmd/up.go | 158 -- cmd/variables.go | 213 -- cmd/version.go | 26 - cmd/whoami.go | 25 - configs/main.go | 136 - configs/project.go | 157 - configs/root.go | 30 - configs/user.go | 32 - constants/docs.go | 3 - constants/url.go | 5 - constants/version.go | 9 - controller/config.go | 49 - controller/deployment.go | 30 - controller/deployment_trigger.go | 20 - controller/down.go | 12 - controller/environment.go | 59 - controller/envs.go | 265 -- controller/logs.go | 149 - controller/main.go | 24 - controller/panic.go | 45 - controller/plugin.go | 19 - controller/project.go | 135 - controller/scope.go | 10 - controller/starter.go | 12 - controller/up.go | 209 -- controller/user.go | 307 -- controller/version.go | 13 - controller/workflow.go | 11 - entity/cobra.go | 10 - entity/config.go | 17 - entity/deployment.go | 45 - entity/deployment_trigger.go | 7 - entity/down.go | 6 - entity/environment.go | 22 - entity/envs.go | 49 - entity/handler.go | 7 - entity/panic.go | 10 - entity/plugin.go | 15 - entity/project.go | 42 - entity/service.go | 6 - entity/starter.go | 14 - entity/up.go | 27 - entity/user.go | 8 - entity/workflow.go | 25 - errors/main.go | 40 - flake.lock | 191 ++ flake.nix | 122 + gateway/deployment.go | 82 - gateway/deployment_trigger.go | 32 - gateway/down.go | 34 - gateway/environment.go | 81 - gateway/envs.go | 96 - gateway/gitignore.go | 224 -- gateway/main.go | 193 -- gateway/panic.go | 33 - gateway/plugin.go | 54 - gateway/project.go | 312 -- gateway/scope.go | 27 - gateway/starter.go | 30 - gateway/up.go | 64 - gateway/user.go | 77 - gateway/workflow.go | 31 - go.mod | 34 - go.sum | 384 --- install.sh | 653 +++-- lib/gql/gql.go | 41 - main.go | 361 --- npm-install/config.js | 47 - npm-install/postinstall.js | 56 - package-lock.json | 256 -- package.json | 29 - random/main.go | 78 - shell.nix | 29 + src/client.rs | 69 + src/commands/add.rs | 128 + src/commands/completion.rs | 21 + src/commands/delete.rs | 113 + src/commands/docs.rs | 27 + src/commands/domain.rs | 115 + src/commands/environment.rs | 82 + src/commands/init.rs | 139 + src/commands/link.rs | 255 ++ src/commands/list.rs | 105 + src/commands/login.rs | 256 ++ src/commands/logout.rs | 13 + src/commands/logs.rs | 83 + src/commands/mod.rs | 27 + src/commands/open.rs | 25 + src/commands/run.rs | 137 + src/commands/service.rs | 80 + src/commands/shell.rs | 109 + src/commands/starship.rs | 13 + src/commands/status.rs | 56 + src/commands/unlink.rs | 86 + src/commands/up.rs | 164 ++ src/commands/variables.rs | 167 ++ src/commands/whoami.rs | 22 + src/config.rs | 283 ++ src/consts.rs | 14 + src/gql/mod.rs | 3 + src/gql/mutations/mod.rs | 57 + .../strings/LoginSessionConsume.graphql | 3 + .../strings/LoginSessionCreate.graphql | 3 + .../mutations/strings/PluginCreate.graphql | 5 + .../mutations/strings/PluginDelete.graphql | 3 + .../mutations/strings/ProjectCreate.graphql | 16 + .../strings/ServiceDomainCreate.graphql | 8 + .../strings/ValidateTwoFactor.graphql | 3 + src/gql/queries/mod.rs | 92 + src/gql/queries/strings/BuildLogs.graphql | 6 + src/gql/queries/strings/Deployments.graphql | 12 + src/gql/queries/strings/Domains.graphql | 18 + src/gql/queries/strings/Project.graphql | 30 + .../queries/strings/ProjectPlugins.graphql | 14 + src/gql/queries/strings/ProjectToken.graphql | 13 + src/gql/queries/strings/Projects.graphql | 23 + src/gql/queries/strings/TwoFactorInfo.graphql | 6 + src/gql/queries/strings/UserMeta.graphql | 6 + src/gql/queries/strings/UserProjects.graphql | 34 + src/gql/queries/strings/Variables.graphql | 13 + src/gql/schema.graphql | 2117 ++++++++++++++ src/gql/subscriptions/mod.rs | 17 + .../subscriptions/strings/BuildLogs.graphql | 10 + .../strings/DeploymentLogs.graphql | 14 + src/macros.rs | 35 + src/main.rs | 65 + src/subscription.rs | 52 + src/table.rs | 176 ++ src/util/mod.rs | 2 + src/util/prompt.rs | 28 + src/util/tokio_spawner.rs | 21 + ui/prompt.go | 308 -- ui/spinner.go | 59 - ui/text.go | 218 -- ui/text_test.go | 252 -- ui/tty.go | 10 - uninstall.sh | 3 - uuid/main.go | 11 - 177 files changed, 9196 insertions(+), 8223 deletions(-) create mode 100644 .github/changelog-configuration.json delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/tag.yml delete mode 100644 .goreleaser.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 Makefile delete mode 100644 bin/railway.js delete mode 100644 cmd/add.go delete mode 100644 cmd/build.go delete mode 100644 cmd/completion.go delete mode 100644 cmd/connect.go delete mode 100644 cmd/delete.go delete mode 100644 cmd/design.go delete mode 100644 cmd/docs.go delete mode 100644 cmd/down.go delete mode 100644 cmd/environment.go delete mode 100644 cmd/init.go delete mode 100644 cmd/link.go delete mode 100644 cmd/list.go delete mode 100644 cmd/login.go delete mode 100644 cmd/logout.go delete mode 100644 cmd/logs.go delete mode 100644 cmd/main.go delete mode 100644 cmd/open.go delete mode 100644 cmd/panic.go delete mode 100644 cmd/protect.go delete mode 100644 cmd/run.go delete mode 100644 cmd/shell.go delete mode 100644 cmd/status.go delete mode 100644 cmd/unlink.go delete mode 100644 cmd/up.go delete mode 100644 cmd/variables.go delete mode 100644 cmd/version.go delete mode 100644 cmd/whoami.go delete mode 100644 configs/main.go delete mode 100644 configs/project.go delete mode 100644 configs/root.go delete mode 100644 configs/user.go delete mode 100644 constants/docs.go delete mode 100644 constants/url.go delete mode 100644 constants/version.go delete mode 100644 controller/config.go delete mode 100644 controller/deployment.go delete mode 100644 controller/deployment_trigger.go delete mode 100644 controller/down.go delete mode 100644 controller/environment.go delete mode 100644 controller/envs.go delete mode 100644 controller/logs.go delete mode 100644 controller/main.go delete mode 100644 controller/panic.go delete mode 100644 controller/plugin.go delete mode 100644 controller/project.go delete mode 100644 controller/scope.go delete mode 100644 controller/starter.go delete mode 100644 controller/up.go delete mode 100644 controller/user.go delete mode 100644 controller/version.go delete mode 100644 controller/workflow.go delete mode 100644 entity/cobra.go delete mode 100644 entity/config.go delete mode 100644 entity/deployment.go delete mode 100644 entity/deployment_trigger.go delete mode 100644 entity/down.go delete mode 100644 entity/environment.go delete mode 100644 entity/envs.go delete mode 100644 entity/handler.go delete mode 100644 entity/panic.go delete mode 100644 entity/plugin.go delete mode 100644 entity/project.go delete mode 100644 entity/service.go delete mode 100644 entity/starter.go delete mode 100644 entity/up.go delete mode 100644 entity/user.go delete mode 100644 entity/workflow.go delete mode 100644 errors/main.go create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 gateway/deployment.go delete mode 100644 gateway/deployment_trigger.go delete mode 100644 gateway/down.go delete mode 100644 gateway/environment.go delete mode 100644 gateway/envs.go delete mode 100644 gateway/gitignore.go delete mode 100644 gateway/main.go delete mode 100644 gateway/panic.go delete mode 100644 gateway/plugin.go delete mode 100644 gateway/project.go delete mode 100644 gateway/scope.go delete mode 100644 gateway/starter.go delete mode 100644 gateway/up.go delete mode 100644 gateway/user.go delete mode 100644 gateway/workflow.go delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 lib/gql/gql.go delete mode 100644 main.go delete mode 100644 npm-install/config.js delete mode 100644 npm-install/postinstall.js delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 random/main.go create mode 100644 shell.nix create mode 100644 src/client.rs create mode 100644 src/commands/add.rs create mode 100644 src/commands/completion.rs create mode 100644 src/commands/delete.rs create mode 100644 src/commands/docs.rs create mode 100644 src/commands/domain.rs create mode 100644 src/commands/environment.rs create mode 100644 src/commands/init.rs create mode 100644 src/commands/link.rs create mode 100644 src/commands/list.rs create mode 100644 src/commands/login.rs create mode 100644 src/commands/logout.rs create mode 100644 src/commands/logs.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/open.rs create mode 100644 src/commands/run.rs create mode 100644 src/commands/service.rs create mode 100644 src/commands/shell.rs create mode 100644 src/commands/starship.rs create mode 100644 src/commands/status.rs create mode 100644 src/commands/unlink.rs create mode 100644 src/commands/up.rs create mode 100644 src/commands/variables.rs create mode 100644 src/commands/whoami.rs create mode 100644 src/config.rs create mode 100644 src/consts.rs create mode 100644 src/gql/mod.rs create mode 100644 src/gql/mutations/mod.rs create mode 100644 src/gql/mutations/strings/LoginSessionConsume.graphql create mode 100644 src/gql/mutations/strings/LoginSessionCreate.graphql create mode 100644 src/gql/mutations/strings/PluginCreate.graphql create mode 100644 src/gql/mutations/strings/PluginDelete.graphql create mode 100644 src/gql/mutations/strings/ProjectCreate.graphql create mode 100644 src/gql/mutations/strings/ServiceDomainCreate.graphql create mode 100644 src/gql/mutations/strings/ValidateTwoFactor.graphql create mode 100644 src/gql/queries/mod.rs create mode 100644 src/gql/queries/strings/BuildLogs.graphql create mode 100644 src/gql/queries/strings/Deployments.graphql create mode 100644 src/gql/queries/strings/Domains.graphql create mode 100644 src/gql/queries/strings/Project.graphql create mode 100644 src/gql/queries/strings/ProjectPlugins.graphql create mode 100644 src/gql/queries/strings/ProjectToken.graphql create mode 100644 src/gql/queries/strings/Projects.graphql create mode 100644 src/gql/queries/strings/TwoFactorInfo.graphql create mode 100644 src/gql/queries/strings/UserMeta.graphql create mode 100644 src/gql/queries/strings/UserProjects.graphql create mode 100644 src/gql/queries/strings/Variables.graphql create mode 100644 src/gql/schema.graphql create mode 100644 src/gql/subscriptions/mod.rs create mode 100644 src/gql/subscriptions/strings/BuildLogs.graphql create mode 100644 src/gql/subscriptions/strings/DeploymentLogs.graphql create mode 100644 src/macros.rs create mode 100644 src/main.rs create mode 100644 src/subscription.rs create mode 100644 src/table.rs create mode 100644 src/util/mod.rs create mode 100644 src/util/prompt.rs create mode 100644 src/util/tokio_spawner.rs delete mode 100644 ui/prompt.go delete mode 100644 ui/spinner.go delete mode 100644 ui/text.go delete mode 100644 ui/text_test.go delete mode 100644 ui/tty.go delete mode 100644 uninstall.sh delete mode 100644 uuid/main.go diff --git a/.github/changelog-configuration.json b/.github/changelog-configuration.json new file mode 100644 index 0000000..1f99ead --- /dev/null +++ b/.github/changelog-configuration.json @@ -0,0 +1,11 @@ +{ + "categories": [ + { + "title": "## Changes", + "labels": [], + "exhaustive": false + } + ], + "template": "${{CHANGELOG}}\n", + "pr_template": "- #${{NUMBER}} ${{TITLE}}" +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8640999..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Set up Go 1.x - uses: actions/setup-go@v2 - with: - go-version: ^1.13 - id: go - - - name: Checkout code - uses: actions/checkout@v2 - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Build - run: make build - - # REMOVE WHEN RESOLVED - # 1) https://github.com/golangci/golangci-lint-action/issues/135 - # 2) https://github.com/golangci/golangci-lint-action/issues/81 - - name: Clean modcache - run: go clean -modcache - - - name: Lint CLI - uses: golangci/golangci-lint-action@v2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..38b0246 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main + +name: CI + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: Swatinem/rust-cache@v2 + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + # Set linting rules for clippy + args: --all-targets + + test-plan: + name: Tests + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + outputs: + matrix: ${{ steps.docker-prep.outputs.matrix }} + if: "!contains(github.event.head_commit.message, '(cargo-release)')" + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: Swatinem/rust-cache@v2 + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.github/workflows/label-check.yml b/.github/workflows/label-check.yml index dd9937a..65104c1 100644 --- a/.github/workflows/label-check.yml +++ b/.github/workflows/label-check.yml @@ -11,6 +11,6 @@ jobs: - uses: jesusvasquez333/verify-pr-label-action@v1.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - valid-labels: "release/patch, release/minor, release/major" - pull-request-number: '${{ github.event.pull_request.number }}' + valid-labels: "release/patch, release/minor, release/major, release/skip" + pull-request-number: "${{ github.event.pull_request.number }}" disable-reviews: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 539f90c..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Publish - -on: - repository_dispatch: - types: [publish-event] - -jobs: - release_and_brew: - name: Release and bump Brew - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - id: go - with: - go-version: ^1.13 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - - - name: Bump Homebrew Core - uses: dawidd6/action-homebrew-bump-formula@v3 - with: - token: ${{ secrets.GH_PAT }} - formula: railway - tag: ${{ github.event.client_payload.new-tag }} - revision: ${{ github.event.client_payload.sha }} - - publish_npm: - name: Publish to NPM - needs: release_and_brew - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set version - id: vars - run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} - - - name: Check version - run: echo "Version ${{ github.event.client_payload.new-tag }}" - - - name: Use Node.js 16 - uses: actions/setup-node@v1 - with: - node-version: 16 - registry-url: https://registry.npmjs.org/ - - - name: Setup Git user - run: | - git config --global user.name "Github Bot" - git config --global user.email "github-bot@railway.app" - - - name: Create .npmrc file - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Bump NPM version - run: npm --no-git-tag-version --allow-same-version version ${{ github.event.client_payload.new-tag }} - - - name: NPM publish - run: npm publish --access public - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Discord Deployment Status Notification - uses: sarisia/actions-status-discord@v1 - with: - webhook: ${{ secrets.DEPLOY_WEBHOOK }} - status: ${{ job.status }} - title: "Published CLI" - description: "Published CLI version ${{ github.event.client_payload.new-tag }} to Brew and NPM" - nofail: false - nodetail: false - username: Github Actions - avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..05a76ec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,154 @@ +name: Release +on: + push: + tags: + - "v*.*.*" +env: + MACOSX_DEPLOYMENT_TARGET: 10.7 + +jobs: + create-release: + name: Create Release + runs-on: ubuntu-latest + outputs: + rlwy_version: ${{ env.CLI_VERSION }} + + steps: + - name: Get the release version from the tag + shell: bash + if: env.CLI_VERSION == '' + run: | + # Apparently, this is the right way to get a tag name. Really? + # + # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 + echo "CLI_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + echo "version is: ${{ env.CLI_VERSION }}" + + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Build Changelog + id: build_changelog + uses: mikepenz/release-changelog-builder-action@v3.7.0 + with: + configuration: ".github/changelog-configuration.json" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create GitHub release + id: release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.CLI_VERSION }} + release_name: ${{ env.CLI_VERSION }} + + build-release: + name: Build Release Assets + needs: ["create-release"] + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + + - target: i686-unknown-linux-musl + os: ubuntu-latest + + - target: aarch64-unknown-linux-musl + os: ubuntu-latest + + - target: arm-unknown-linux-musleabihf + os: ubuntu-latest + + - target: x86_64-apple-darwin + os: macOS-latest + + - target: aarch64-apple-darwin + os: macOS-latest + + - target: x86_64-pc-windows-msvc + os: windows-latest + + - target: i686-pc-windows-msvc + os: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + profile: minimal + override: true + + - name: Build release binary + uses: actions-rs/cargo@v1 + with: + command: build + args: --release --locked --target ${{ matrix.target }} + use-cross: ${{ matrix.os == 'ubuntu-latest' }} + + - name: Prepare binaries [Windows] + if: matrix.os == 'windows-latest' + run: | + cd target/${{ matrix.target }}/release + strip rlwy.exe + 7z a ../../../rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}.zip rlwy.exe + cd - + + - name: Prepare binaries [-linux] + if: matrix.os != 'windows-latest' + run: | + cd target/${{ matrix.target }}/release + strip rlwy || true + tar czvf ../../../rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}.tar.gz rlwy + cd - + + - name: Upload release archive + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.create-release.outputs.rlwy_version }} + files: rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}* + + - name: Install cargo-deb + if: matrix.target == 'x86_64-unknown-linux-musl' + run: cargo install cargo-deb + + - name: Generate .deb package file + if: matrix.target == 'x86_64-unknown-linux-musl' + run: cargo deb --target x86_64-unknown-linux-musl --output rlwy-${{ needs.create-release.outputs.rlwy_version }}-amd64.deb + + - name: Upload .deb package file + if: matrix.target == 'x86_64-unknown-linux-musl' + uses: svenstaro/upload-release-action@v2 + with: + tag: ${{ needs.create-release.outputs.rlwy_version }} + file: rlwy-${{ needs.create-release.outputs.rlwy_version }}-amd64.deb + + - name: Update homebrew tap + uses: mislav/bump-homebrew-formula-action@v2 + if: "matrix.target == 'x86_64-apple-darwin' || matrix.target == 'aarch64-apple-darwin' && !contains(github.ref, '-')" + with: + formula-name: rlwy + formula-path: rlwy.rb + homebrew-tap: railwayapp/homebrew-tap + download-url: https://github.com/railwayapp/cli/releases/latest/download/rlwy-${{ needs.create-release.outputs.rlwy_version }}-${{ matrix.target }}.tar.gz + env: + COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml deleted file mode 100644 index edf2236..0000000 --- a/.github/workflows/tag.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Tag - -on: - push: - branches: - - master - -jobs: - tag: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: actions-ecosystem/action-get-merged-pull-request@v1 - id: get-merged-pull-request - with: - github_token: ${{ secrets.GH_PAT }} - - - uses: actions-ecosystem/action-release-label@v1 - id: release-label - if: ${{ steps.get-merged-pull-request.outputs.title != null }} - with: - github_token: ${{ secrets.GH_PAT }} - labels: ${{ steps.get-merged-pull-request.outputs.labels }} - - - uses: actions-ecosystem/action-get-latest-tag@v1 - id: get-latest-tag - if: ${{ steps.release-label.outputs.level != null }} - with: - semver_only: true - - - uses: actions-ecosystem/action-bump-semver@v1 - id: bump-semver - if: ${{ steps.release-label.outputs.level != null }} - with: - current_version: ${{ steps.get-latest-tag.outputs.tag }} - level: ${{ steps.release-label.outputs.level }} - - - uses: actions-ecosystem/action-regex-match@v2 - id: regex-match - if: ${{ steps.bump-semver.outputs.new_version != null }} - with: - text: ${{ steps.get-merged-pull-request.outputs.body }} - regex: '```release_note([\s\S]*)```' - - - uses: actions-ecosystem/action-push-tag@v1 - if: ${{ steps.bump-semver.outputs.new_version != null }} - with: - tag: ${{ steps.bump-semver.outputs.new_version }} - message: "${{ steps.bump-semver.outputs.new_version }}: PR #${{ steps.get-merged-pull-request.outputs.number }} ${{ steps.get-merged-pull-request.outputs.title }}" - - - uses: peter-evans/repository-dispatch@v1 - if: ${{ steps.bump-semver.outputs.new_version != null }} - with: - token: ${{ secrets.GH_PAT }} - repository: railwayapp/cli - event-type: publish-event - client-payload: '{"new-tag": "${{ steps.bump-semver.outputs.new_version }}", "release-notes": "${{ steps.regex-match.outputs.group1 }}"}' diff --git a/.gitignore b/.gitignore index 886bc68..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,147 +1 @@ -# Created by https://www.toptal.com/developers/gitignore/api/go,node -# Edit at https://www.toptal.com/developers/gitignore?templates=go,node - -### Go ### -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -### Go Patch ### -/vendor/ -/Godeps/ - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# End of https://www.toptal.com/developers/gitignore/api/go,node - -# Build directory -bin/* -!bin/*.js - -# NPM token -.npmrc - -# Railway directories -.railway - -# bin -cli +/target diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 7f12761..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Documentation at http://goreleaser.com -project_name: railway - -before: - hooks: - - go mod download - -builds: - - binary: railway - env: - - CGO_ENABLED=0 - ldflags: - - -s -w -X github.com/railwayapp/cli/constants.Version={{.Version}} - goos: - - linux - - windows - - darwin - -brews: - - tap: - owner: railwayapp - name: homebrew-railway - - commit_author: - name: goreleaserbot - email: goreleaser@railway.app - - homepage: "https://railway.app" - description: "Develop and deploy code with zero configuration" - - install: | - bin.install "railway" - -snapshot: - name_template: "{{ .Tag }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef1bd5e..5d08316 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,15 +1,17 @@ # Contribute to the Railway CLI -## Setup +## Prerequisites +- [Rust](https://www.rust-lang.org/tools/install) +- [CMake](https://cmake.org/install/) -Run +OR -```shell -make run -``` +- [Nix](https://nixos.org/download.html) -Build -```shell -make build -``` +### Nix Setup +`nix-shell` to enter the shell with all the dependencies + +### Running and Building +`cargo run -- ` to run the binary\ +`cargo build --release` to build the binary \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0184d7f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2519 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "async-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b750efd83b7e716a015eed5ebb583cda83c52d9b24a8f0125e5c48c3313c9f8" +dependencies = [ + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tungstenite", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "box_drawing" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea27d8d5fd867b17523bf6788b1175fa9867f34669d057e9adaf76e27bcea44b" + +[[package]] +name = "bstr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "serde", + "winapi", +] + +[[package]] +name = "clap" +version = "4.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_complete" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0012995dc3a54314f4710f5631d74767e73c534b8757221708303e48eef7a19b" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "console" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.42.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "core_affinity" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4436406e93f52cce33bfba4be067a9f7229da44a634c385e4b22cdfaca5f84cc" +dependencies = [ + "libc", + "num_cpus", + "winapi", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f67c7faacd4db07a939f55d66a983a5355358a1f17d32cc9a8d01d1266b9ce" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cxx" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0808e1bd8671fb44a113a14e13497557533369847788fa2ae912b6ebfce9fa8" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dyn-clone" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b0705efd4599c15a38151f4721f7bc388306f61084d3bfd50bd07fbca5cb60" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "filetime" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.45.0", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.5", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" + +[[package]] +name = "futures-macro" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures 0.1.31", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", + "tokio-io", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "graphql-introspection-query" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d" +dependencies = [ + "serde", +] + +[[package]] +name = "graphql-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" +dependencies = [ + "combine", + "thiserror", +] + +[[package]] +name = "graphql-ws-client" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a663387e7f23153b3b6fe78d64004d84cf93b142bb3ebe5b98745a03a3244ec" +dependencies = [ + "async-tungstenite", + "futures 0.3.26", + "graphql_client", + "log", + "pin-project", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "graphql_client" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc16d75d169fddb720d8f1c7aed6413e329e1584079b9734ff07266a193f5bc" +dependencies = [ + "graphql_query_derive", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "graphql_client_codegen" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f290ecfa3bea3e8a157899dc8a1d96ee7dd6405c18c8ddd213fc58939d18a0e9" +dependencies = [ + "graphql-introspection-query", + "graphql-parser", + "heck", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "graphql_query_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a755cc59cda2641ea3037b4f9f7ef40471c329f55c1fa2db6fa0bb7ae6c1f7ce" +dependencies = [ + "graphql_client_codegen", + "proc-macro2", + "syn", +] + +[[package]] +name = "gzp" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c65d1899521a11810501b50b898464d133e1afc96703cff57726964cfa7baf" +dependencies = [ + "byteorder", + "bytes 1.4.0", + "core_affinity", + "flate2", + "flume", + "num_cpus", + "thiserror", +] + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes 1.4.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes 1.4.0", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes 1.4.0", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d" +dependencies = [ + "bytes 1.4.0", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92445bc9cc14bfa0a3ce56817dc3b5bcc227a168781a356b702410789cec0d10" +dependencies = [ + "bytes 1.4.0", + "futures-util", + "http", + "http-body 1.0.0-rc.2", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +dependencies = [ + "bytes 1.4.0", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body 0.4.5", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75264b2003a3913f118d35c586e535293b3e22e41f074930762929d071e092" +dependencies = [ + "bytes 1.4.0", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body 1.0.0-rc.2", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper 0.14.24", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "indoc" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2b9d82064e8a0226fddb3547f37f28eaa46d0fc210e275d835f08cf3b76a7" + +[[package]] +name = "inquire" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a94f0659efe59329832ba0452d3ec753145fc1fb12a8e1d60de4ccf99f5364" +dependencies = [ + "bitflags", + "crossterm 0.25.0", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" + +[[package]] +name = "is-terminal" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.45.0", +] + +[[package]] +name = "names" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc" +dependencies = [ + "rand", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "paste" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "railwayapp" +version = "3.0.0" +dependencies = [ + "anyhow", + "async-tungstenite", + "base64 0.21.0", + "box_drawing", + "chrono", + "clap", + "clap_complete", + "colored", + "console", + "crossterm 0.26.0", + "dirs", + "futures 0.3.26", + "graphql-ws-client", + "graphql_client", + "gzp", + "hostname", + "http-body-util", + "httparse", + "hyper 1.0.0-rc.3", + "ignore", + "indicatif", + "indoc", + "inquire", + "is-terminal", + "names", + "num_cpus", + "open", + "paste", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_with", + "synchronized-writer", + "tar", + "textwrap", + "tokio", + "tokio-stream", + "tui", + "url", + "uuid", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "reqwest" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +dependencies = [ + "base64 0.21.0", + "bytes 1.4.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body 0.4.5", + "hyper 0.14.24", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.0", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d904179146de381af4c93d3af6ca4984b3152db687dacb9c3c35e86f39809c" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1966009f3c05f095697c537312f5415d1e3ed31ce0a56942bac4c771c5c335e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +dependencies = [ + "lock_api", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synchronized-writer" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3543ca0810e71767052bdcdd5653f23998b192642a22c5164bfa6581e40a4a2" + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes 1.4.0", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes 1.4.0", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.25.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "tungstenite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes 1.4.0", + "http", + "httparse", + "log", + "rand", + "rustls", + "sha-1", + "thiserror", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d4ad514 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "railwayapp" +version = "3.0.0" +edition = "2021" +license = "MIT" +authors = ["Railway "] +description = "Interact with Railway via CLI" +readme = "README.md" +homepage = "https://github.com/railwayapp/cli" +repository = "https://github.com/railwayapp/cli" +rust-version = "1.67.1" +default-run = "railway" + +[[bin]] +name = "railway" +path = "src/main.rs" + +[[bin]] +name = "rlwy" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.69" +clap = { version = "4.1.6", features = ["derive", "suggestions"] } +colored = "2.0.0" +dirs = "4.0.0" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" +reqwest = { version = "0.11.14", default-features = false, features = [ + "rustls-tls", +] } +chrono = { version = "0.4.23", features = ["serde"], default-features = false } +graphql_client = { version = "0.11.0", features = ["reqwest-rustls"] } +paste = "1.0.11" +tokio = { version = "1.25.0", features = ["full"] } +clap_complete = "4.1.3" +open = "3.2.0" +inquire = "0.5.3" +tui = "0.19.0" +crossterm = "0.26.0" +hyper = { version = "1.0.0-rc.3", features = ["server", "http1"] } +base64 = "0.21.0" +http-body-util = "0.1.0-rc.2" +rand = "0.8.5" +hostname = "0.3.1" +indicatif = "0.17.3" +indoc = "2.0.0" +console = "0.15.5" +box_drawing = "0.1.2" +textwrap = "0.16.0" +gzp = { version = "0.11.3", default-features = false, features = [ + "deflate_rust", +] } +tar = "0.4.38" +synchronized-writer = "1.1.11" +ignore = "0.4.20" +num_cpus = "1.15.0" +url = "2.3.1" +futures = { version = "0.3.26", default-features = false, features = [ + "compat", + "io-compat", +] } +tokio-stream = { version = "0.1.12", default-features = false, features = [ + "sync", +] } +uuid = { version = "1.3.0", features = ["serde", "v4"] } +httparse = "1.8.0" +names = { version = "0.14.0", default-features = false } +graphql-ws-client = { version = "0.3.0", features = ["client-graphql-client"] } +async-tungstenite = { version = "0.18.0", features = [ + "tokio-runtime", + "tokio-rustls-native-certs", +] } +is-terminal = "0.4.4" +serde_with = "2.2.0" diff --git a/LICENSE b/LICENSE index 5f56296..ea15886 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Railway Corp. +Copyright (c) 2023 Railway Corp. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ 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. +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index c947773..0000000 --- a/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -build: - @go build -o bin/railway - -run: - @go run main.go diff --git a/README.md b/README.md index c866cd0..4ce22a9 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,33 @@ -# Railway CLI +# Railway CLI (3.x.x) -![Build](https://github.com/railwayapp/cli/workflows/Build/badge.svg) +[![CI](https://github.com/railwayapp/cliv3/actions/workflows/ci.yml/badge.svg)](https://github.com/railwayapp/cliv3/actions/workflows/ci.yml) -This is the command line interface for [Railway](https://railway.app). Use it to connect your code to Railways infrastructure without needing to worry about environment variables or configuration. +This is the command line interface for [Railway](https://railway.app). Use it to connect your code to Railway's infrastructure without needing to worry about environment variables or configuration. [View the docs](https://docs.railway.app/develop/cli) +The Railway command line interface (CLI) connects your code to your Railway project from the command line. + +The Railway CLI allows you to + +- Create new Railway projects from the terminal +- Link to an existing Railway project +- Pull down environment variables for your project locally to run +- Create services and databases right from the comfort of your fingertips + ## Installation - -The Railway CLI is available through [Homebrew](https://brew.sh/), [NPM](https://www.npmjs.com/package/@railway/cli), curl, or as a [Nixpkg](https://nixos.org). - -### Brew - -```shell -brew install railway +### Cargo +```bash +cargo install railwayapp --locked ``` - -### NPM - -```shell -npm i -g @railway/cli -``` - -### Yarn - -```shell -yarn global add @railway/cli -``` - -### curl - -```shell -curl -fsSL https://railway.app/install.sh | sh -``` - -### Nixpkg -Note: This installation method is not supported by Railway and is maintained by the community. -```shell -# On NixOS -nix-env -iA nixos.railway -# On non-NixOS -nix-env -iA nixpkgs.railway -``` - ### From source -See [CONTRIBUTING.md](https://github.com/railwayapp/cli/blob/master/CONTRIBUTING.md) for information on setting up this repo locally. +See [CONTRIBUTING.md](https://github.com/railwayapp/cliv3/blob/master/CONTRIBUTING.md) for information on setting up this repo locally. ## Documentation - [View the full documentation](https://docs.railway.app) ## Feedback -We would love to hear your feedback or suggestions. The best way to reach us is on [Discord](https://discord.gg/xAm2w6g). +We would love to hear your feedback or suggestions. The best way to reach us is on [Discord](https://discord.gg/railway). -We also welcome pull requests into this repo. See [CONTRIBUTING.md](https://github.com/railwayapp/cli/blob/master/CONTRIBUTING.md) for information on setting up this repo locally. +We also welcome pull requests into this repo. See [CONTRIBUTING.md](https://github.com/railwayapp/cliv3/blob/master/CONTRIBUTING.md) for information on setting up this repo locally. diff --git a/bin/railway.js b/bin/railway.js deleted file mode 100644 index a3a3c9d..0000000 --- a/bin/railway.js +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node -import { execFileSync } from "child_process"; -import path from "path"; -import { exit } from "process"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -try { - execFileSync(path.resolve(`${__dirname}/railway`), process.argv.slice(2), { - stdio: "inherit", - }); -} catch (e) { - exit(1) -} - diff --git a/cmd/add.go b/cmd/add.go deleted file mode 100644 index 984b94b..0000000 --- a/cmd/add.go +++ /dev/null @@ -1,39 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Add(ctx context.Context, req *entity.CommandRequest) error { - projectCfg, err := h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return err - } - plugins, err := h.ctrl.GetAvailablePlugins(ctx, projectCfg.Project) - if err != nil { - return err - } - selectedPlugin, err := ui.PromptPlugins(plugins) - if err != nil { - return err - } - ui.StartSpinner(&ui.SpinnerCfg{ - Message: fmt.Sprintf("Adding %s plugin", selectedPlugin), - }) - defer ui.StopSpinner("") - - plugin, err := h.ctrl.CreatePlugin(ctx, &entity.CreatePluginRequest{ - ProjectID: projectCfg.Project, - Plugin: selectedPlugin, - }) - if err != nil { - return err - } - fmt.Printf("🎉 Created plugin %s\n", ui.MagentaText(plugin.Name)) - return nil - -} diff --git a/cmd/build.go b/cmd/build.go deleted file mode 100644 index 90098c8..0000000 --- a/cmd/build.go +++ /dev/null @@ -1,24 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Build(ctx context.Context, req *entity.CommandRequest) error { - if h.cfg.RailwayProductionToken == "" { - fmt.Println("Railway env file is only generated in production") - return nil - } - - err := h.ctrl.SaveEnvsToFile(ctx) - if err != nil { - return err - } - - fmt.Printf(`Env written to %s -Do NOT commit the env.json file. This command should only be run as a production build step.\n`, h.cfg.RailwayEnvFilePath) - return nil -} diff --git a/cmd/completion.go b/cmd/completion.go deleted file mode 100644 index 8e0eb4c..0000000 --- a/cmd/completion.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "context" - "os" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Completion(ctx context.Context, req *entity.CommandRequest) error { - switch req.Args[0] { - case "bash": - err := req.Cmd.Root().GenBashCompletion(os.Stdout) - if err != nil { - return err - } - case "zsh": - err := req.Cmd.Root().GenZshCompletion(os.Stdout) - if err != nil { - return err - } - case "fish": - err := req.Cmd.Root().GenFishCompletion(os.Stdout, true) - if err != nil { - return err - } - case "powershell": - err := req.Cmd.Root().GenPowerShellCompletion(os.Stdout) - if err != nil { - return err - } - } - return nil -} diff --git a/cmd/connect.go b/cmd/connect.go deleted file mode 100644 index 9c4b3ad..0000000 --- a/cmd/connect.go +++ /dev/null @@ -1,162 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - "os/exec" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Connect(ctx context.Context, req *entity.CommandRequest) error { - projectCfg, _ := h.ctrl.GetProjectConfigs(ctx) - - project, err := h.ctrl.GetProject(ctx, projectCfg.Project) - if err != nil { - return err - } - - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - fmt.Printf("🎉 Connecting to: %s %s\n", ui.MagentaText(project.Name), ui.MagentaText(environment.Name)) - - var plugin string - - if len(req.Args) == 0 { - names := make([]string, 0) - for _, plugin := range project.Plugins { - // TODO: Better way of handling this - if plugin.Name != "env" { - names = append(names, plugin.Name) - } - } - - fmt.Println("Select a database to connect to:") - plugin, err = ui.PromptPlugins(names) - if err != nil { - return err - } - } else { - plugin = req.Args[0] - } - - if !isPluginValid(plugin) { - return fmt.Errorf("Invalid plugin: %s", plugin) - } - envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, nil) - if err != nil { - return err - } - - command, connectEnv := buildConnectCommand(plugin, envs) - if !commandExistsInPath(command[0]) { - fmt.Println("🚨", ui.RedText(command[0]), "was not found in $PATH.") - return nil - } - - cmd := exec.CommandContext(ctx, command[0], command[1:]...) - - cmd.Env = os.Environ() - for k, v := range connectEnv { - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%+v", k, v)) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdout - cmd.Stdin = os.Stdin - catchSignals(ctx, cmd, nil) - - err = cmd.Run() - if err != nil { - return err - } - - return nil -} - -func commandExistsInPath(cmd string) bool { - // The error can be safely ignored because it indicates a failure to find the - // command in $PATH. - _, err := exec.LookPath(cmd) - return err == nil -} - -func isPluginValid(plugin string) bool { - switch plugin { - case "redis": - fallthrough - case "psql": - fallthrough - case "postgres": - fallthrough - case "postgresql": - fallthrough - case "mysql": - fallthrough - case "mongo": - fallthrough - case "mongodb": - return true - default: - return false - } -} - -func buildConnectCommand(plugin string, envs *entity.Envs) ([]string, map[string]string) { - var command []string - var connectEnv map[string]string - - switch plugin { - case "redis": - // run - command = []string{"redis-cli", "-u", (*envs)["REDIS_URL"]} - case "psql": - fallthrough - case "postgres": - fallthrough - case "postgresql": - connectEnv = map[string]string{ - "PGPASSWORD": (*envs)["PGPASSWORD"], - } - command = []string{ - "psql", - "-U", - (*envs)["PGUSER"], - "-h", - (*envs)["PGHOST"], - "-p", - (*envs)["PGPORT"], - "-d", - (*envs)["PGDATABASE"], - } - case "mongo": - fallthrough - case "mongodb": - command = []string{ - "mongo", - fmt.Sprintf( - "mongodb://%s:%s@%s:%s", - (*envs)["MONGOUSER"], - (*envs)["MONGOPASSWORD"], - (*envs)["MONGOHOST"], - (*envs)["MONGOPORT"], - ), - } - case "mysql": - command = []string{ - "mysql", - fmt.Sprintf("-h%s", (*envs)["MYSQLHOST"]), - fmt.Sprintf("-u%s", (*envs)["MYSQLUSER"]), - fmt.Sprintf("-p%s", (*envs)["MYSQLPASSWORD"]), - fmt.Sprintf("--port=%s", (*envs)["MYSQLPORT"]), - "--protocol=TCP", - (*envs)["MYSQLDATABASE"], - } - } - return command, connectEnv -} diff --git a/cmd/delete.go b/cmd/delete.go deleted file mode 100644 index b3b66d3..0000000 --- a/cmd/delete.go +++ /dev/null @@ -1,113 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" - "github.com/railwayapp/cli/uuid" -) - -func (h *Handler) Delete(ctx context.Context, req *entity.CommandRequest) error { - user, err := h.ctrl.GetUser(ctx) - if err != nil { - return err - } - if user.Has2FA { - fmt.Printf("Your account has 2FA enabled, you must delete your project on the Dashboard.") - return nil - } - - if len(req.Args) > 0 { - // projectID provided as argument - arg := req.Args[0] - - if uuid.IsValidUUID(arg) { - project, err := h.ctrl.GetProject(ctx, arg) - if err != nil { - return err - } - - return h.ctrl.DeleteProject(ctx, project.Id) - } - - project, err := h.ctrl.GetProjectByName(ctx, arg) - if err != nil { - return err - } - - return h.ctrl.DeleteProject(ctx, project.Id) - } - - isLoggedIn, err := h.ctrl.IsLoggedIn(ctx) - if err != nil { - return err - } - - if isLoggedIn { - return h.deleteFromAccount(ctx, req) - } - - return h.deleteFromID(ctx, req) -} - -func (h *Handler) deleteFromAccount(ctx context.Context, req *entity.CommandRequest) error { - projects, err := h.ctrl.GetProjects(ctx) - if err != nil { - return err - } - - if len(projects) == 0 { - fmt.Printf("No Projects found.") - return nil - } - - project, err := ui.PromptProjects(projects) - if err != nil { - return err - } - name, err := ui.PromptConfirmProjectName() - if err != nil { - return err - } - if project.Name != name { - fmt.Printf("The project name typed doesn't match the selected project to be deleted.") - return nil - } - fmt.Printf("🔥 Deleting project %s\n", ui.MagentaText(name)) - err = h.deleteById(ctx, project.Id) - if err != nil { - return err - } - fmt.Printf("✅ Deleted project %s\n", ui.MagentaText(name)) - return nil -} - -func (h *Handler) deleteFromID(ctx context.Context, req *entity.CommandRequest) error { - projectID, err := ui.PromptText("Enter your project id") - if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, projectID) - - if err != nil { - return err - } - fmt.Printf("🔥 Deleting project %s\n", ui.MagentaText(project.Name)) - err = h.deleteById(ctx, project.Id) - if err != nil { - return err - } - fmt.Printf("✅ Deleted project %s\n", ui.MagentaText(project.Name)) - return nil -} - -func (h *Handler) deleteById(ctx context.Context, projectId string) error { - err := h.ctrl.DeleteProject(ctx, projectId) - if err != nil { - return err - } - return nil -} diff --git a/cmd/design.go b/cmd/design.go deleted file mode 100644 index 55439c0..0000000 --- a/cmd/design.go +++ /dev/null @@ -1,67 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Design(ctx context.Context, req *entity.CommandRequest) error { - fmt.Print(ui.Heading("Alerts")) - fmt.Print(ui.AlertDanger("Something bad is going to happen!")) - fmt.Print(ui.AlertWarning("That might not have been what you wanted")) - fmt.Print(ui.AlertInfo("Just so you know you know, Railway is awesome")) - - fmt.Println("") - fmt.Print(ui.Heading("Unordered List")) - fmt.Print(ui.UnorderedList([]string{ - "List Item 1", - "List Item 2", - "List Item 3", - })) - - fmt.Println("") - fmt.Print(ui.Heading("Ordered List")) - fmt.Print(ui.OrderedList([]string{ - "First Step", - "Next Step", - })) - - fmt.Println("") - fmt.Print(ui.Heading("Key-Value Pairs")) - fmt.Print(ui.KeyValues(map[string]string{ - "First Key": "Value 1", - "Second Key": "Value 2", - "Third Key": "Value 3", - })) - - fmt.Println("") - fmt.Print(ui.Heading("Truncated Text")) - fmt.Println(ui.Truncate("012345678901234567890123456789", 50)) - fmt.Println(ui.Truncate("012345678901234567890123456789", 10)) - fmt.Println(ui.Truncate("012345678901234567890123456789", 0)) - - fmt.Println("") - fmt.Print(ui.Heading("Secret Text")) - fmt.Printf("My super secret password is %s, the name of my childhood pet.\n", ui.ObscureText("Luke")) - - fmt.Println("") - fmt.Print(ui.Heading("Paragraph")) - fmt.Print(ui.Paragraph("Paragraphs print the given text, but wrap it automatically when the lines are too long. It's super convenient!")) - - fmt.Println("") - fmt.Print(ui.Heading("Indented")) - fmt.Print(ui.Indent("func main() {\n println(\"Hello World!\")\n}")) - - fmt.Println("") - fmt.Print(ui.Heading("Block Quote")) - fmt.Print(ui.BlockQuote("That's the thing about counter-intuitive ideas. They contradict your intuitions. So, they seem wrong")) - - fmt.Println("") - fmt.Print(ui.Heading("Generic Line Prefix")) - fmt.Print(ui.PrefixLines("Line 1\nLine 2\nLine 3", "🥳 ")) - - return nil -} diff --git a/cmd/docs.go b/cmd/docs.go deleted file mode 100644 index 1161d1b..0000000 --- a/cmd/docs.go +++ /dev/null @@ -1,12 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/railwayapp/cli/constants" - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Docs(ctx context.Context, req *entity.CommandRequest) error { - return h.ctrl.ConfirmBrowserOpen("Opening Railway Docs...", constants.RailwayDocsURL) -} diff --git a/cmd/down.go b/cmd/down.go deleted file mode 100644 index 37d0f85..0000000 --- a/cmd/down.go +++ /dev/null @@ -1,68 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Down(ctx context.Context, req *entity.CommandRequest) error { - isVerbose, err := req.Cmd.Flags().GetBool("verbose") - - if err != nil { - // Verbose mode isn't a necessary flag; just default to false. - isVerbose = false - } - - fmt.Print(ui.VerboseInfo(isVerbose, "Using verbose mode")) - - fmt.Print(ui.VerboseInfo(isVerbose, "Loading project configuration")) - projectConfig, err := h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return err - } - - fmt.Print(ui.VerboseInfo(isVerbose, "Loading environment")) - environmentName, err := req.Cmd.Flags().GetString("environment") - if err != nil { - return err - } - - environment, err := h.getEnvironment(ctx, environmentName) - if err != nil { - return err - } - fmt.Print(ui.VerboseInfo(isVerbose, fmt.Sprintf("Using environment %s", ui.Bold(environment.Name)))) - - fmt.Print(ui.VerboseInfo(isVerbose, "Loading project")) - project, err := h.ctrl.GetProject(ctx, projectConfig.Project) - if err != nil { - return err - } - - bypass, err := req.Cmd.Flags().GetBool("yes") - if err != nil { - bypass = false - } - if !bypass { - shouldDelete, err := ui.PromptYesNo(fmt.Sprintf("Delete latest deployment for project %s?", project.Name)) - if err != nil || !shouldDelete { - return err - } - } - - err = h.ctrl.Down(ctx, &entity.DownRequest{ - ProjectID: project.Id, - EnvironmentID: environment.Id, - }) - - if err != nil { - return err - } - - fmt.Print(ui.AlertInfo(fmt.Sprintf("Deleted latest deployment for project %s.", project.Name))) - - return nil -} diff --git a/cmd/environment.go b/cmd/environment.go deleted file mode 100644 index 1c137ad..0000000 --- a/cmd/environment.go +++ /dev/null @@ -1,62 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/manifoldco/promptui" - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Environment(ctx context.Context, req *entity.CommandRequest) error { - projectID, err := h.cfg.GetProject() - if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, projectID) - if err != nil { - return err - } - - var environment *entity.Environment - if len(req.Args) > 0 { - var name = req.Args[0] - - // Look for existing environment with name - for _, projectEnvironment := range project.Environments { - if name == projectEnvironment.Name { - environment = projectEnvironment - } - } - - if (environment != nil) { - fmt.Printf("%s Environment: %s\n", promptui.IconGood, ui.BlueText(environment.Name)) - } else { - // Create new environment - environment, err = h.ctrl.CreateEnvironment(ctx, &entity.CreateEnvironmentRequest{ - Name: name, - ProjectID: project.Id, - }) - if err != nil { - return err - } - fmt.Printf("Created Environment %s\nEnvironment: %s\n", promptui.IconGood, ui.BlueText(ui.Bold(name).String())) - } - } else { - // Existing environment selector - environment, err = ui.PromptEnvironments(project.Environments) - if err != nil { - return err - } - } - - err = h.cfg.SetEnvironment(environment.Id) - if err != nil { - return err - } - - fmt.Printf("%s ProTip: You can view the active environment by running %s\n", promptui.IconInitial, ui.BlueText("railway status")) - return err -} diff --git a/cmd/init.go b/cmd/init.go deleted file mode 100644 index 75660aa..0000000 --- a/cmd/init.go +++ /dev/null @@ -1,250 +0,0 @@ -package cmd - -import ( - "context" - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/railwayapp/cli/entity" - CLIErrors "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) initNew(ctx context.Context, req *entity.CommandRequest) error { - name, err := ui.PromptProjectName() - if err != nil { - return err - } - - project, err := h.ctrl.CreateProject(ctx, &entity.CreateProjectRequest{ - Name: &name, - }) - if err != nil { - return err - } - - err = h.cfg.SetNewProject(project.Id) - if err != nil { - return err - } - - environment, err := ui.PromptEnvironments(project.Environments) - if err != nil { - return err - } - - err = h.cfg.SetEnvironment(environment.Id) - if err != nil { - return err - } - - // Check if a .env exists, if so prompt uploading it - err = h.ctrl.AutoImportDotEnv(ctx) - if err != nil { - return err - } - - fmt.Printf("🎉 Created project %s\n", ui.MagentaText(name)) - - return h.ctrl.OpenProjectInBrowser(ctx, project.Id, environment.Id) -} - -func (h *Handler) initFromTemplate(ctx context.Context, req *entity.CommandRequest) error { - ui.StartSpinner(&ui.SpinnerCfg{ - Message: "Fetching starter templates", - }) - - starters, err := h.ctrl.GetStarters(ctx) - if err != nil { - return err - } - ui.StopSpinner("") - - template, err := ui.PromptStarterTemplates(starters) - if err != nil { - return err - } - - // Parse to get query params - parsedUrl, err := url.ParseQuery(template.Url) - if err != nil { - return err - } - - optionalEnvVars := parsedUrl.Get("optionalEnvs") - envVars := strings.Split(parsedUrl.Get("envs"), ",") - plugins := strings.Split(parsedUrl.Get("plugins"), ",") - - // Prepare environment variables for prompt - starterEnvVars := make([]*entity.StarterEnvVar, 0) - for _, variable := range envVars { - if variable != "" { - var envVar = new(entity.StarterEnvVar) - envVar.Name = variable - envVar.Desc = parsedUrl.Get(variable + "Desc") - envVar.Default = parsedUrl.Get(variable + "Default") - envVar.Optional = strings.Contains(optionalEnvVars, variable) - - starterEnvVars = append(starterEnvVars, envVar) - } - } - - // Prepare plugins for creation - starterPlugins := make([]string, 0) - for _, plugin := range plugins { - if plugin != "" { - starterPlugins = append(starterPlugins, plugin) - } - } - - // Select GitHub owner - ui.StartSpinner(&ui.SpinnerCfg{ - Message: "Fetching GitHub scopes", - }) - scopes, err := h.ctrl.GetWritableGithubScopes(ctx) - if err != nil { - return err - } - if len(scopes) == 0 { - return CLIErrors.NoGitHubScopesFound - } - ui.StopSpinner("") - - owner, err := ui.PromptGitHubScopes(scopes) - if err != nil { - return err - } - - // Enter project name - name, err := ui.PromptProjectName() - if err != nil { - return err - } - - isPrivate, err := ui.PromptIsRepoPrivate() - if err != nil { - return err - } - - // Prompt for env vars (if required) - variables, err := ui.PromptEnvVars(starterEnvVars) - if err != nil { - return err - } - - // Create Railway project - ui.StartSpinner(&ui.SpinnerCfg{ - Message: "Creating project", - }) - creationResult, err := h.ctrl.CreateProjectFromTemplate(ctx, &entity.CreateProjectFromTemplateRequest{ - Name: name, - Owner: owner, - Template: template.Source, - IsPrivate: isPrivate, - Plugins: starterPlugins, - Variables: variables, - }) - if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, creationResult.ProjectID) - if err != nil { - return err - } - - ui.StopSpinner("") - - // Wait for workflow to complete - ui.StartSpinner(&ui.SpinnerCfg{ - Message: "Deploying project", - }) - - for { - time.Sleep(2 * time.Second) - workflowStatus, err := h.ctrl.GetWorkflowStatus(ctx, creationResult.WorkflowID) - if err != nil { - return err - } - if workflowStatus.IsError() { - ui.StopSpinner("Uhh Ohh. Workflow failed!") - return CLIErrors.WorkflowFailed - } - if workflowStatus.IsComplete() { - ui.StopSpinner("Project creation complete 🚀") - break - } - } - - // Select environment to activate - environment, err := ui.PromptEnvironments(project.Environments) - if err != nil { - return err - } - - err = h.cfg.SetEnvironment(environment.Id) - if err != nil { - return err - } - - // Check if a .env exists, if so prompt uploading it - err = h.ctrl.AutoImportDotEnv(ctx) - if err != nil { - return err - } - - fmt.Printf("🎉 Created project %s\n", ui.MagentaText(name)) - return h.ctrl.OpenProjectDeploymentsInBrowser(ctx, project.Id) -} - -func (h *Handler) setProject(ctx context.Context, project *entity.Project) error { - err := h.cfg.SetNewProject(project.Id) - if err != nil { - return err - } - - environment, err := ui.PromptEnvironments(project.Environments) - if err != nil { - return err - } - - err = h.cfg.SetEnvironment(environment.Id) - if err != nil { - return err - } - - return nil -} - -func (h *Handler) Init(ctx context.Context, req *entity.CommandRequest) error { - if len(req.Args) > 0 { - // NOTE: This is to support legacy `railway init ` which should - // now be `railway link ` - return h.Link(ctx, req) - } - - // Since init can be called by guests, ensure we can fetch a user first before calling. This prevents - // us accidentally creating a temporary (guest) project if we have a token locally but our remote - // session was deleted. - _, err := h.ctrl.GetUser(ctx) - if err != nil { - return fmt.Errorf("%s\nRun %s", ui.RedText("Account required to init project"), ui.Bold("railway login")) - } - - selection, err := ui.PromptInit() - if err != nil { - return err - } - - switch selection { - case ui.InitNew: - return h.initNew(ctx, req) - case ui.InitFromTemplate: - return h.initFromTemplate(ctx, req) - default: - return errors.New("Invalid selection") - } -} diff --git a/cmd/link.go b/cmd/link.go deleted file mode 100644 index 3d9a26c..0000000 --- a/cmd/link.go +++ /dev/null @@ -1,79 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" - "github.com/railwayapp/cli/uuid" -) - -func (h *Handler) Link(ctx context.Context, req *entity.CommandRequest) error { - if len(req.Args) > 0 { - // projectID provided as argument - arg := req.Args[0] - - if uuid.IsValidUUID(arg) { - project, err := h.ctrl.GetProject(ctx, arg) - if err != nil { - return err - } - - return h.setProject(ctx, project) - } - - project, err := h.ctrl.GetProjectByName(ctx, arg) - if err != nil { - return err - } - - return h.setProject(ctx, project) - } - - isLoggedIn, err := h.ctrl.IsLoggedIn(ctx) - if err != nil { - return err - } - - if isLoggedIn { - return h.linkFromAccount(ctx, req) - } else { - return h.linkFromID(ctx, req) - } -} - -func (h *Handler) linkFromAccount(ctx context.Context, _ *entity.CommandRequest) error { - projects, err := h.ctrl.GetProjects(ctx) - if err != nil { - return err - } - - if len(projects) == 0 { - fmt.Print(ui.AlertWarning("No projects found")) - fmt.Printf("Create one with %s\n", ui.GreenText("railway init")) - os.Exit(1) - } - - project, err := ui.PromptProjects(projects) - if err != nil { - return err - } - - return h.setProject(ctx, project) -} - -func (h *Handler) linkFromID(ctx context.Context, _ *entity.CommandRequest) error { - projectID, err := ui.PromptText("Enter your project id") - if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, projectID) - if err != nil { - return err - } - - return h.setProject(ctx, project) -} diff --git a/cmd/list.go b/cmd/list.go deleted file mode 100644 index 00badd6..0000000 --- a/cmd/list.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) List(ctx context.Context, req *entity.CommandRequest) error { - projectId, err := h.cfg.GetProject() - if err != nil { - return err - } - projects, err := h.ctrl.GetProjects(ctx) - if err != nil { - return err - } - - for _, v := range projects { - if projectId == v.Id { - fmt.Println(ui.MagentaText(v.Name)) - continue - } - fmt.Println(ui.GrayText(v.Name)) - } - - return nil -} diff --git a/cmd/login.go b/cmd/login.go deleted file mode 100644 index 890f14c..0000000 --- a/cmd/login.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Login(ctx context.Context, req *entity.CommandRequest) error { - isBrowserless, err := req.Cmd.Flags().GetBool("browserless") - if err != nil { - return err - } - - user, err := h.ctrl.Login(ctx, isBrowserless) - if err != nil { - return err - } - - fmt.Printf("\n🎉 Logged in as %s (%s)\n", ui.Bold(user.Name), user.Email) - - return nil -} diff --git a/cmd/logout.go b/cmd/logout.go deleted file mode 100644 index 40c51a0..0000000 --- a/cmd/logout.go +++ /dev/null @@ -1,11 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Logout(ctx context.Context, req *entity.CommandRequest) error { - return h.ctrl.Logout(ctx) -} diff --git a/cmd/logs.go b/cmd/logs.go deleted file mode 100644 index 552ffa2..0000000 --- a/cmd/logs.go +++ /dev/null @@ -1,15 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Logs(ctx context.Context, req *entity.CommandRequest) error { - numLines, err := req.Cmd.Flags().GetInt32("lines") - if err != nil { - return err - } - return h.ctrl.GetActiveDeploymentLogs(ctx, numLines) -} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 5a43872..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,18 +0,0 @@ -package cmd - -import ( - "github.com/railwayapp/cli/configs" - "github.com/railwayapp/cli/controller" -) - -type Handler struct { - ctrl *controller.Controller - cfg *configs.Configs -} - -func New() *Handler { - return &Handler{ - ctrl: controller.New(), - cfg: configs.New(), - } -} diff --git a/cmd/open.go b/cmd/open.go deleted file mode 100644 index 1f6af6b..0000000 --- a/cmd/open.go +++ /dev/null @@ -1,47 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Open(ctx context.Context, req *entity.CommandRequest) error { - projectId, err := h.cfg.GetProject() - if err != nil { - return err - } - environmentId, err := h.cfg.GetCurrentEnvironment() - if err != nil { - return err - } - - // If an unknown subcommand is used, show help - if len(req.Args) > 0 { - return req.Cmd.Help() - } - - if req.Cmd.Use == "open" { - return h.ctrl.OpenProjectInBrowser(ctx, projectId, environmentId) - } - - return h.ctrl.OpenProjectPathInBrowser(ctx, projectId, environmentId, req.Cmd.Use) -} - -func (h *Handler) OpenApp(ctx context.Context, req *entity.CommandRequest) error { - projectId, err := h.cfg.GetProject() - if err != nil { - return err - } - environmentId, err := h.cfg.GetCurrentEnvironment() - if err != nil { - return err - } - - deployment, err := h.ctrl.GetLatestDeploymentForEnvironment(ctx, projectId, environmentId) - if err != nil { - return err - } - - return h.ctrl.OpenStaticUrlInBrowser(deployment.StaticUrl) -} diff --git a/cmd/panic.go b/cmd/panic.go deleted file mode 100644 index d58ac49..0000000 --- a/cmd/panic.go +++ /dev/null @@ -1,30 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - - "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Panic(ctx context.Context, panicErr string, stacktrace string, cmd string, args []string) error { - cmd = cmd + " " + strings.Join(args, " ") - for _, arg := range args { - if arg == "-v" { - // Verbose mode show err - fmt.Println(panicErr, stacktrace) - } - } - - success, err := h.ctrl.SendPanic(ctx, panicErr, stacktrace, cmd) - if err != nil { - return err - } - if success { - ui.StopSpinner("Successfully sent the error! We're figuring out what went wrong.") - return nil - } - return errors.TelemetryFailed -} diff --git a/cmd/protect.go b/cmd/protect.go deleted file mode 100644 index 6289e2a..0000000 --- a/cmd/protect.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Protect(ctx context.Context, req *entity.CommandRequest) error { - projectConfigs, err := h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return err - } - - mp := make(map[string]bool) - - for k, v := range projectConfigs.LockedEnvsNames { - mp[k] = v - } - - mp[projectConfigs.Environment] = true - - projectConfigs.LockedEnvsNames = mp - - err = h.cfg.SetProjectConfigs(projectConfigs) - if err != nil { - return err - } - - return err -} diff --git a/cmd/run.go b/cmd/run.go deleted file mode 100644 index e44b96d..0000000 --- a/cmd/run.go +++ /dev/null @@ -1,301 +0,0 @@ -package cmd - -import ( - "context" - goErr "errors" - "fmt" - "net" - "os" - "os/exec" - "os/signal" - "regexp" - "strconv" - "strings" - "syscall" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -var RAIL_PORT = 4411 - -func (h *Handler) getEnvironment(ctx context.Context, environmentName string) (*entity.Environment, error) { - if environmentName == "" { - return h.ctrl.GetCurrentEnvironment(ctx) - } - return h.ctrl.GetEnvironmentByName(ctx, environmentName) -} - -func (h *Handler) Run(ctx context.Context, req *entity.CommandRequest) error { - isEphemeral := false - - for _, arg := range req.Args { - if arg == "--ephemeral" { - isEphemeral = true - } - } - - parsedArgs := make([]string, 0) - - rgxEnvironment, err := regexp.Compile("--environment=(.*)") - if err != nil { - return err - } - - rgxService, err := regexp.Compile("--service=(.*)") - if err != nil { - return err - } - - targetEnvironment := "" - var targetServiceName *string - - // Parse --environment={ENV} and --service={SERVICE} from args - for _, arg := range req.Args { - if matched := rgxEnvironment.FindStringSubmatch(arg); matched != nil { - if len(matched) < 2 { - return goErr.New("missing environment selection! \n(e.g --environment=production)") - } - targetEnvironment = matched[1] - } else if matched := rgxService.FindStringSubmatch(arg); matched != nil { - if len(matched) < 2 { - return goErr.New("missing service selection! \n(e.g --service=serviceName)") - } - targetServiceName = &matched[1] - } else { - parsedArgs = append(parsedArgs, arg) - } - } - - projectCfg, err := h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return err - } - - environment, err := h.getEnvironment(ctx, targetEnvironment) - if err != nil { - return err - } - // Add something to the ephemeral env name - if isEphemeral { - environmentName := fmt.Sprintf("%s-ephemeral", environment.Name) - fmt.Printf("Spinning up Ephemeral Environment: %s\n", ui.BlueText(environmentName)) - // Create new environment for this run - environment, err = h.ctrl.CreateEphemeralEnvironment(ctx, &entity.CreateEphemeralEnvironmentRequest{ - Name: environmentName, - ProjectID: projectCfg.Project, - BaseEnvironmentID: environment.Id, - }) - if err != nil { - return err - } - fmt.Println("Done!") - } - envs, err := h.ctrl.GetEnvs(ctx, environment, targetServiceName) - - if err != nil { - return err - } - - pwd, err := os.Getwd() - if err != nil { - return err - } - - hasDockerfile := true - - if _, err := os.Stat(fmt.Sprintf("%s/Dockerfile", pwd)); os.IsNotExist(err) { - hasDockerfile = false - } - - if len(parsedArgs) == 0 && hasDockerfile { - return h.runInDocker(ctx, pwd, envs) - } else if len(parsedArgs) == 0 { - return errors.CommandNotSpecified - } - - cmd := exec.CommandContext(ctx, parsedArgs[0], parsedArgs[1:]...) - cmd.Env = os.Environ() - - // Inject railway envs - for k, v := range *envs { - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%+v", k, v)) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdout - cmd.Stdin = os.Stdin - catchSignals(ctx, cmd, nil) - - err = cmd.Run() - - if isEphemeral { - // Teardown Environment - fmt.Println("Tearing down ephemeral environment...") - err := h.ctrl.DeleteEnvironment(ctx, &entity.DeleteEnvironmentRequest{ - EnvironmentId: environment.Id, - ProjectID: projectCfg.Project, - }) - if err != nil { - return err - } - fmt.Println("Done!") - } - - if err != nil { - fmt.Println(err.Error()) - if exitError, ok := err.(*exec.ExitError); ok { - os.Exit(exitError.ExitCode()) - } - os.Exit(1) - } - - printLooksGood() - - return nil -} - -func (h *Handler) runInDocker(ctx context.Context, pwd string, envs *entity.Envs) error { - // Start building the image - projectCfg, err := h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, projectCfg.Project) - if err != nil { - return err - } - - // Strip characters not allowed in Docker image names - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - sanitiser := regexp.MustCompile(`[^A-Za-z0-9_-]`) - imageNameWithoutNsOrTag := strings.ToLower(sanitiser.ReplaceAllString(project.Name, "") + "-" + sanitiser.ReplaceAllString(environment.Name, "")) - image := fmt.Sprintf("railway-local/%s:latest", imageNameWithoutNsOrTag) - - buildArgs := []string{"build", "-q", "-t", image, pwd} - - // Build up env - for k, v := range *envs { - buildArgs = append(buildArgs, "--build-arg", fmt.Sprintf("%s=\"%+v\"", k, v)) - } - - buildCmd := exec.CommandContext(ctx, "docker", buildArgs...) - fmt.Printf("Building %s from Dockerfile...\n", ui.GreenText(image)) - - buildCmd.Stdout = os.Stdout - buildCmd.Stderr = os.Stderr - - err = buildCmd.Start() - if err != nil { - return err - } - err = buildCmd.Wait() - if err != nil { - return err - } - fmt.Printf("🎉 Built %s\n", ui.GreenText(image)) - - // Attempt to use - internalPort := envs.Get("PORT") - - externalPort, err := getAvailablePort() - if err != nil { - return err - } - - if internalPort == "" { - internalPort = externalPort - } - - // Start running the image - fmt.Printf("🚂 Running at %s\n\n", ui.GreenText(fmt.Sprintf("127.0.0.1:%s", externalPort))) - - runArgs := []string{"run", "--init", "--rm", "-p", fmt.Sprintf("127.0.0.1:%s:%s", externalPort, internalPort), "-e", fmt.Sprintf("PORT=%s", internalPort), "-d"} - // Build up env - for k, v := range *envs { - runArgs = append(runArgs, "-e", fmt.Sprintf("%s=%+v", k, v)) - } - runArgs = append(runArgs, image) - - // Run the container - rawContainerId, err := exec.CommandContext(ctx, "docker", runArgs...).Output() - if err != nil { - return err - } - - // Get the container ID - containerId := strings.TrimSpace(string(rawContainerId)) - - // Attach to the container - logCmd := exec.CommandContext(ctx, "docker", "logs", "-f", containerId) - logCmd.Stdout = os.Stdout - logCmd.Stderr = os.Stderr - - err = logCmd.Start() - if err != nil { - return err - } - // Listen for cancel to remove the container - catchSignals(ctx, logCmd, func() { - err = exec.Command("docker", "rm", "-f", string(containerId)).Run() - }) - err = logCmd.Wait() - if err != nil && !strings.Contains(err.Error(), "255") { - // 255 is a graceeful exit with ctrl + c - return err - } - - printLooksGood() - - return nil -} - -func getAvailablePort() (string, error) { - searchRange := 64 - for i := RAIL_PORT; i < RAIL_PORT+searchRange; i++ { - if isAvailable(i) { - return strconv.Itoa(i), nil - } - } - return "", fmt.Errorf("Couldn't find available port between %d and %d", RAIL_PORT, RAIL_PORT+searchRange) -} - -func isAvailable(port int) bool { - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - return false - } - _ = ln.Close() - return true -} - -func catchSignals(_ context.Context, cmd *exec.Cmd, onSignal context.CancelFunc) { - sigs := make(chan os.Signal, 1) - - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go func() { - sig := <-sigs - err := cmd.Process.Signal(sig) - if onSignal != nil { - onSignal() - } - if err != nil { - fmt.Println("Child process error: \n", err) - } - }() -} - -func printLooksGood() { - // Get space between last output and this message - fmt.Println() - fmt.Printf( - "🚄 Looks good? Then put it on the train and deploy with `%s`!\n", - ui.GreenText("railway up"), - ) -} diff --git a/cmd/shell.go b/cmd/shell.go deleted file mode 100644 index aff21af..0000000 --- a/cmd/shell.go +++ /dev/null @@ -1,60 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - "os/exec" - "runtime" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Shell(ctx context.Context, req *entity.CommandRequest) error { - serviceName, err := req.Cmd.Flags().GetString("service") - if err != nil { - return err - } - - envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, &serviceName) - if err != nil { - return err - } - - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - shellVar := os.Getenv("SHELL") - if shellVar == "" { - // Fallback shell to use - if isWindows() { - shellVar = "cmd" - } else { - shellVar = "bash" - } - } - - fmt.Print(ui.Paragraph(fmt.Sprintf("Loading subshell with variables from %s", environment.Name))) - - subShellCmd := exec.CommandContext(ctx, shellVar) - subShellCmd.Env = os.Environ() - for k, v := range *envs { - subShellCmd.Env = append(subShellCmd.Env, fmt.Sprintf("%s=%+v", k, v)) - } - - subShellCmd.Stdout = os.Stdout - subShellCmd.Stderr = os.Stderr - subShellCmd.Stdin = os.Stdin - catchSignals(ctx, subShellCmd, nil) - - err = subShellCmd.Run() - - return err -} - -func isWindows() bool { - return runtime.GOOS == "windows" -} diff --git a/cmd/status.go b/cmd/status.go deleted file mode 100644 index f48679a..0000000 --- a/cmd/status.go +++ /dev/null @@ -1,57 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Status(ctx context.Context, req *entity.CommandRequest) error { - projectCfg, err := h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, projectCfg.Project) - if err != nil { - return err - } - - if project != nil { - fmt.Printf("Project: %s\n", ui.Bold(fmt.Sprint(ui.MagentaText(project.Name)))) - - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - fmt.Printf("Environment: %s\n", ui.Bold(fmt.Sprint(ui.BlueText(environment.Name)))) - - if len(project.Plugins) > 0 { - fmt.Printf("Plugins:\n") - for i := range project.Plugins { - plugin := project.Plugins[i] - if plugin.Name == "env" { - // legacy plugin - continue - } - fmt.Printf("%s\n", ui.Bold(fmt.Sprint(ui.GrayText(plugin.Name)))) - } - } - - if len(project.Services) > 0 { - fmt.Printf("Services:\n") - for i := range project.Services { - fmt.Printf("%s\n", ui.Bold(fmt.Sprint(ui.GrayText(project.Services[i].Name)))) - } - } - } else { - fmt.Println(errors.ProjectConfigNotFound) - } - - return nil - -} diff --git a/cmd/unlink.go b/cmd/unlink.go deleted file mode 100644 index 3287845..0000000 --- a/cmd/unlink.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "github.com/railwayapp/cli/errors" - "os" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Unlink(ctx context.Context, _ *entity.CommandRequest) error { - projectCfg, err := h.ctrl.GetProjectConfigs(ctx) - if err == errors.ProjectConfigNotFound { - fmt.Print(ui.AlertWarning("No project is currently linked")) - os.Exit(1) - } else if err != nil { - return err - } - - project, err := h.ctrl.GetProject(ctx, projectCfg.Project) - if err != nil { - return err - } - - err = h.cfg.RemoveProjectConfigs(projectCfg) - if err != nil { - return err - } - - fmt.Printf("🎉 Disconnected from %s\n", ui.MagentaText(project.Name)) - return nil -} diff --git a/cmd/up.go b/cmd/up.go deleted file mode 100644 index 97c36b0..0000000 --- a/cmd/up.go +++ /dev/null @@ -1,158 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "io/ioutil" - "time" - - "github.com/railwayapp/cli/entity" - CLIErrors "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Up(ctx context.Context, req *entity.CommandRequest) error { - isVerbose, err := req.Cmd.Flags().GetBool("verbose") - if err != nil { - // Verbose mode isn't a necessary flag; just default to false. - isVerbose = false - } - - serviceName, err := req.Cmd.Flags().GetString("service") - if err != nil { - return err - } - - fmt.Print(ui.VerboseInfo(isVerbose, "Using verbose mode")) - - projectConfig, err := h.linkAndGetProjectConfigs(ctx, req) - if err != nil { - return err - } - - src := projectConfig.ProjectPath - if src == "" { - // When deploying with a project token, the project path is empty - src = "." - } - - fmt.Print(ui.VerboseInfo(isVerbose, fmt.Sprintf("Uploading directory %s", src))) - - fmt.Print(ui.VerboseInfo(isVerbose, "Loading environment")) - environmentName, err := req.Cmd.Flags().GetString("environment") - if err != nil { - return err - } - - environment, err := h.getEnvironment(ctx, environmentName) - if err != nil { - return err - } - fmt.Print(ui.VerboseInfo(isVerbose, fmt.Sprintf("Using environment %s", ui.Bold(environment.Name)))) - - fmt.Print(ui.VerboseInfo(isVerbose, "Loading project")) - project, err := h.ctrl.GetProject(ctx, projectConfig.Project) - if err != nil { - return err - } - - serviceId := "" - if serviceName != "" { - for _, service := range project.Services { - if service.Name == serviceName { - serviceId = service.ID - } - } - - if serviceId == "" { - return CLIErrors.ServiceNotFound - } - } - - // If service has not been provided via flag, prompt for it - if serviceId == "" { - fmt.Print(ui.VerboseInfo(isVerbose, "Loading services")) - service, err := ui.PromptServices(project.Services) - if err != nil { - return err - } - - if service != nil { - serviceId = service.ID - } - } - - _, err = ioutil.ReadFile(".railwayignore") - if err == nil { - fmt.Print(ui.VerboseInfo(isVerbose, "Using ignore file .railwayignore")) - } - - ui.StartSpinner(&ui.SpinnerCfg{ - Message: "Laying tracks in the clouds...", - }) - res, err := h.ctrl.Upload(ctx, &entity.UploadRequest{ - ProjectID: projectConfig.Project, - EnvironmentID: environment.Id, - ServiceID: serviceId, - RootDir: src, - }) - if err != nil { - ui.StopSpinner("") - return err - } else { - ui.StopSpinner(fmt.Sprintf("☁️ Build logs available at %s\n", ui.GrayText(res.URL))) - } - detach, err := req.Cmd.Flags().GetBool("detach") - if err != nil { - return err - } - if detach { - return nil - } - - for i := 0; i < 3; i++ { - err = h.ctrl.GetActiveBuildLogs(ctx, 0) - if err == nil { - break - } - time.Sleep(time.Duration(i) * 250 * time.Millisecond) - } - - fmt.Printf("\n\n======= Build Completed ======\n\n") - - err = h.ctrl.GetActiveDeploymentLogs(ctx, 1000) - if err != nil { - return err - } - - fmt.Printf("☁️ Deployment logs available at %s\n", ui.GrayText(res.URL)) - fmt.Printf("OR run `railway logs` to tail them here\n\n") - - if res.DeploymentDomain != "" { - fmt.Printf("☁️ Deployment live at %s\n", ui.GrayText(h.ctrl.GetFullUrlFromStaticUrl(res.DeploymentDomain))) - } else { - fmt.Printf("☁️ Deployment is live\n") - } - - return nil -} - -func (h *Handler) linkAndGetProjectConfigs(ctx context.Context, req *entity.CommandRequest) (*entity.ProjectConfig, error) { - projectConfig, err := h.ctrl.GetProjectConfigs(ctx) - if err == CLIErrors.ProjectConfigNotFound { - // If project isn't configured, prompt to link and do it again - err := h.linkFromAccount(ctx, req) - if err != nil { - return nil, err - } - - projectConfig, err = h.ctrl.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - - return projectConfig, nil -} diff --git a/cmd/variables.go b/cmd/variables.go deleted file mode 100644 index e3d7a61..0000000 --- a/cmd/variables.go +++ /dev/null @@ -1,213 +0,0 @@ -package cmd - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/railwayapp/cli/ui" - - "github.com/railwayapp/cli/entity" -) - -func (h *Handler) Variables(ctx context.Context, req *entity.CommandRequest) error { - serviceName, err := req.Cmd.Flags().GetString("service") - if err != nil { - return err - } - - envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, &serviceName) - if err != nil { - return err - } - - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - fmt.Print(ui.Heading(fmt.Sprintf("%s Environment Variables", environment.Name))) - fmt.Print(ui.KeyValues(*envs)) - - return nil -} - -func (h *Handler) VariablesGet(ctx context.Context, req *entity.CommandRequest) error { - serviceName, err := req.Cmd.Flags().GetString("service") - if err != nil { - return err - } - - envs, err := h.ctrl.GetEnvsForCurrentEnvironment(ctx, &serviceName) - if err != nil { - return err - } - - for _, key := range req.Args { - fmt.Println(envs.Get(key)) - } - - return nil -} - -func (h *Handler) VariablesSet(ctx context.Context, req *entity.CommandRequest) error { - serviceName, err := req.Cmd.Flags().GetString("service") - if err != nil { - return err - } - - skipRedeploy, err := req.Cmd.Flags().GetBool("skip-redeploy") - if err != nil { - // The flag is optional; default to false. - skipRedeploy = false - } - - replace, err := req.Cmd.Flags().GetBool("replace") - if err != nil { - // The flag is optional; default to false. - replace = false - } - - yes, err := req.Cmd.Flags().GetBool("yes") - if err != nil { - // The flag is optional; default to false. - yes = false - } - - if replace && !yes { - fmt.Println(ui.Bold(ui.RedText(fmt.Sprintf("Warning! You are about to fully replace all your variables for the service '%s'.", serviceName)).String())) - confirm, err := ui.PromptYesNo("Continue?") - if err != nil { - return err - } - if !confirm { - return nil - } - } - - variables := &entity.Envs{} - updatedEnvNames := make([]string, 0) - - for _, kvPair := range req.Args { - parts := strings.SplitN(kvPair, "=", 2) - if len(parts) != 2 { - return errors.New("invalid variables invocation. See --help") - } - key := parts[0] - value := parts[1] - - variables.Set(key, value) - updatedEnvNames = append(updatedEnvNames, key) - } - - err = h.ctrl.UpdateEnvs(ctx, variables, &serviceName, replace) - - if err != nil { - return err - } - - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - operation := "Updated" - if replace { - operation = "Replaced existing variables with" - } - - fmt.Print(ui.Heading(fmt.Sprintf("%s %s for \"%s\"", operation, strings.Join(updatedEnvNames, ", "), environment.Name))) - fmt.Print(ui.KeyValues(*variables)) - - if !skipRedeploy { - serviceID, err := h.ctrl.GetServiceIdByName(ctx, &serviceName) - if err != nil { - return err - } - - err = h.redeployAfterVariablesChange(ctx, environment, serviceID) - if err != nil { - return err - } - } - - return nil -} - -func (h *Handler) VariablesDelete(ctx context.Context, req *entity.CommandRequest) error { - serviceName, err := req.Cmd.Flags().GetString("service") - if err != nil { - return err - } - - skipRedeploy, err := req.Cmd.Flags().GetBool("skip-redeploy") - if err != nil { - // The flag is optional; default to false. - skipRedeploy = false - } - - err = h.ctrl.DeleteEnvs(ctx, req.Args, &serviceName) - if err != nil { - return err - } - - environment, err := h.ctrl.GetCurrentEnvironment(ctx) - if err != nil { - return err - } - - fmt.Print(ui.Heading(fmt.Sprintf("Deleted %s for \"%s\"", strings.Join(req.Args, ", "), environment.Name))) - - if !skipRedeploy { - serviceID, err := h.ctrl.GetServiceIdByName(ctx, &serviceName) - if err != nil { - return err - } - - err = h.redeployAfterVariablesChange(ctx, environment, serviceID) - if err != nil { - return err - } - } - - return nil -} - -func (h *Handler) redeployAfterVariablesChange(ctx context.Context, environment *entity.Environment, serviceID *string) error { - deployments, err := h.ctrl.GetDeployments(ctx) - if err != nil { - return err - } - - // Don't redeploy if we don't yet have any deployments - if len(deployments) == 0 { - return nil - } - - // Don't redeploy if the latest deploy for environment came from up - latestDeploy := deployments[0] - if latestDeploy.Meta == nil || latestDeploy.Meta.Repo == "" { - fmt.Printf(ui.AlertInfo("Run %s to redeploy your project"), ui.MagentaText("railway up").Underline()) - return nil - } - - ui.StartSpinner(&ui.SpinnerCfg{ - Message: fmt.Sprintf("Redeploying \"%s\" with new variables", environment.Name), - }) - - err = h.ctrl.DeployEnvironmentTriggers(ctx, serviceID) - if err != nil { - return err - } - - ui.StopSpinner("Deploy triggered") - - deployment, err := h.ctrl.GetLatestDeploymentForEnvironment(ctx, latestDeploy.ProjectID, environment.Id) - if err != nil { - return err - } - - fmt.Printf("☁️ Deploy Logs available at %s\n", ui.GrayText(h.ctrl.GetServiceDeploymentsURL(ctx, latestDeploy.ProjectID, *serviceID, deployment.ID))) - return nil -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index c7288f3..0000000 --- a/cmd/version.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/constants" - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Version(ctx context.Context, req *entity.CommandRequest) error { - fmt.Printf("railway version %s\n", ui.MagentaText(constants.Version)) - return nil -} - -func (h *Handler) CheckVersion(ctx context.Context, req *entity.CommandRequest) error { - if constants.Version != constants.VersionDefault { - latest, _ := h.ctrl.GetLatestVersion() - // Suppressing error as getting latest version is desired, not required - if latest != "" && latest[1:] != constants.Version { - fmt.Println(ui.Bold(fmt.Sprintf("A newer version of the Railway CLI is available, please update to: %s", ui.MagentaText(latest)))) - } - } - return nil -} diff --git a/cmd/whoami.go b/cmd/whoami.go deleted file mode 100644 index 0b1f581..0000000 --- a/cmd/whoami.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (h *Handler) Whoami(ctx context.Context, req *entity.CommandRequest) error { - user, err := h.ctrl.GetUser(ctx) - if err != nil { - return err - } - - userText := fmt.Sprintf("%s", ui.MagentaText(user.Email)) - if user.Name != "" { - userText = fmt.Sprintf("%s (%s)", user.Name, ui.MagentaText(user.Email)) - } - fmt.Printf("👋 Hey %s\n", userText) - - // Todo, more info, also more fun - return nil -} diff --git a/configs/main.go b/configs/main.go deleted file mode 100644 index 72a5b09..0000000 --- a/configs/main.go +++ /dev/null @@ -1,136 +0,0 @@ -package configs - -import ( - "fmt" - "os" - "path" - "path/filepath" - "reflect" - - "github.com/railwayapp/cli/constants" - "github.com/spf13/viper" -) - -type Config struct { - viper *viper.Viper - configPath string -} - -type Configs struct { - rootConfigs *Config - projectConfigs *Config - RailwayProductionToken string - RailwayEnvFilePath string -} - -func IsDevMode() bool { - environment, exists := os.LookupEnv("RAILWAY_ENV") - return exists && environment == "develop" -} - -func IsStagingMode() bool { - environment, exists := os.LookupEnv("RAILWAY_ENV") - return exists && environment == "staging" -} - -func GetRailwayURL() string { - url, exists := os.LookupEnv("RAILWAY_URL") - if !exists { - return constants.RAILWAY_URL - } - return url -} - -func (c *Configs) CreatePathIfNotExist(path string) error { - dir := filepath.Dir(path) - - if _, err := os.Stat(dir); os.IsNotExist(err) { - err = os.MkdirAll(dir, os.ModePerm) - if err != nil { - return err - } - } - - return nil -} - -func (c *Configs) marshalConfig(config *Config, cfg interface{}) error { - reflectCfg := reflect.ValueOf(cfg) - for i := 0; i < reflectCfg.NumField(); i++ { - k := reflectCfg.Type().Field(i).Name - v := reflectCfg.Field(i).Interface() - - config.viper.Set(k, v) - } - - err := c.CreatePathIfNotExist(config.configPath) - - if err != nil { - return err - } - - return config.viper.WriteConfig() -} - -func New() *Configs { - // Configs stored in root (~/.railway) - // Includes token, etc - rootViper := viper.New() - rootConfigPartialPath := ".railway/config.json" - if IsDevMode() { - rootConfigPartialPath = ".railway/dev-config.json" - } - - if IsStagingMode() { - rootConfigPartialPath = ".railway/staging-config.json" - } - - homeDir, err := os.UserHomeDir() - if err != nil { - panic(err) - } - - rootConfigPath := path.Join(homeDir, rootConfigPartialPath) - - rootViper.SetConfigFile(rootConfigPath) - err = rootViper.ReadInConfig() - if os.IsNotExist(err) { - // That's okay, configs are created as needed - } else if err != nil { - fmt.Printf("Unable to parse railway config! %s\n", err) - } - - rootConfig := &Config{ - viper: rootViper, - configPath: rootConfigPath, - } - - // Configs stored in projects (/.railway) - // Includes projectId, environmentId, etc - projectDir, err := filepath.Abs("./.railway") - if err != nil { - panic(err) - } - projectViper := viper.New() - - projectPath := path.Join(projectDir, "./config.json") - projectViper.SetConfigFile(projectPath) - err = projectViper.ReadInConfig() - if os.IsNotExist(err) { - // That's okay, configs are created as needed - } else if err != nil { - fmt.Printf("Unable to parse project config! %s\n", err) - } - - projectConfig := &Config{ - viper: projectViper, - configPath: projectPath, - } - - return &Configs{ - projectConfigs: projectConfig, - rootConfigs: rootConfig, - RailwayProductionToken: os.Getenv("RAILWAY_TOKEN"), - RailwayEnvFilePath: path.Join(projectDir, "env.json"), - } -} diff --git a/configs/project.go b/configs/project.go deleted file mode 100644 index f5a73c3..0000000 --- a/configs/project.go +++ /dev/null @@ -1,157 +0,0 @@ -package configs - -import ( - "fmt" - "os" - "strings" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -func (c *Configs) getCWD() (string, error) { - cwd, err := os.Getwd() - if err != nil { - return "", err - } - - return cwd, nil -} - -func (c *Configs) GetProjectConfigs() (*entity.ProjectConfig, error) { - // Ignore error because the config probably doesn't exist yet - // TODO: Better error handling here - - userCfg, err := c.GetRootConfigs() - if err != nil { - return nil, errors.RootConfigNotFound - } - - // lookup project in global config based on pwd - cwd, err := c.getCWD() - if err != nil { - return nil, err - } - - // find longest matching parent path - var longestPath = -1 - var pathMatch = "" - for path := range userCfg.Projects { - var matches = strings.HasPrefix(fmt.Sprintf("%s/", cwd), fmt.Sprintf("%s/", path)) - if matches && len(path) > longestPath { - longestPath = len(path) - pathMatch = path - } - } - - if longestPath == -1 { - return nil, errors.ProjectConfigNotFound - } - - projectCfg, found := userCfg.Projects[pathMatch] - - if !found { - return nil, errors.ProjectConfigNotFound - } - - return &projectCfg, nil -} - -func (c *Configs) SetProjectConfigs(cfg *entity.ProjectConfig) error { - rootCfg, err := c.GetRootConfigs() - if err != nil { - rootCfg = &entity.RootConfig{} - } - - if rootCfg.Projects == nil { - rootCfg.Projects = make(map[string]entity.ProjectConfig) - } - - rootCfg.Projects[cfg.ProjectPath] = *cfg - - return c.SetRootConfig(rootCfg) -} - -func (c *Configs) RemoveProjectConfigs(cfg *entity.ProjectConfig) error { - rootCfg, err := c.GetRootConfigs() - if err != nil { - rootCfg = &entity.RootConfig{} - } - - delete(rootCfg.Projects, cfg.ProjectPath) - - return c.SetRootConfig(rootCfg) -} - -func (c *Configs) createNewProjectConfig() (*entity.ProjectConfig, error) { - cwd, err := c.getCWD() - if err != nil { - return nil, err - } - - projectCfg := &entity.ProjectConfig{ - ProjectPath: cwd, - } - - return projectCfg, nil -} - -func (c *Configs) SetProject(projectID string) error { - projectCfg, err := c.GetProjectConfigs() - - if err != nil { - projectCfg, err = c.createNewProjectConfig() - - if err != nil { - return err - } - } - - projectCfg.Project = projectID - return c.SetProjectConfigs(projectCfg) -} - -// SetNewProject configures railway project for current working directory -func (c *Configs) SetNewProject(projectID string) error { - projectCfg, err := c.createNewProjectConfig() - - if err != nil { - return err - } - - projectCfg.Project = projectID - return c.SetProjectConfigs(projectCfg) -} - -func (c *Configs) SetEnvironment(environmentId string) error { - projectCfg, err := c.GetProjectConfigs() - - if err != nil { - projectCfg, err = c.createNewProjectConfig() - - if err != nil { - return err - } - } - - projectCfg.Environment = environmentId - return c.SetProjectConfigs(projectCfg) -} - -func (c *Configs) GetProject() (string, error) { - projectCfg, err := c.GetProjectConfigs() - if err != nil { - return "", err - } - - return projectCfg.Project, nil -} - -func (c *Configs) GetCurrentEnvironment() (string, error) { - projectCfg, err := c.GetProjectConfigs() - if err != nil { - return "", err - } - - return projectCfg.Environment, nil -} diff --git a/configs/root.go b/configs/root.go deleted file mode 100644 index b810811..0000000 --- a/configs/root.go +++ /dev/null @@ -1,30 +0,0 @@ -package configs - -import ( - "encoding/json" - "github.com/railwayapp/cli/errors" - "io/ioutil" - "os" - - "github.com/railwayapp/cli/entity" -) - -func (c *Configs) GetRootConfigs() (*entity.RootConfig, error) { - var cfg entity.RootConfig - b, err := ioutil.ReadFile(c.rootConfigs.configPath) - if os.IsNotExist(err) { - return nil, errors.RootConfigNotFound - } else if err != nil { - return nil, err - } - err = json.Unmarshal(b, &cfg) - return &cfg, err -} - -func (c *Configs) SetRootConfig(cfg *entity.RootConfig) error { - if cfg.Projects == nil { - cfg.Projects = make(map[string]entity.ProjectConfig) - } - - return c.marshalConfig(c.rootConfigs, *cfg) -} diff --git a/configs/user.go b/configs/user.go deleted file mode 100644 index 311122a..0000000 --- a/configs/user.go +++ /dev/null @@ -1,32 +0,0 @@ -package configs - -import ( - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -func (c *Configs) GetUserConfigs() (*entity.UserConfig, error) { - var rootCfg *entity.RootConfig - rootCfg, err := c.GetRootConfigs() - if err != nil { - return nil, errors.UserConfigNotFound - } - - if rootCfg.User.Token == "" { - return nil, errors.UserConfigNotFound - } - - return &rootCfg.User, nil -} - -func (c *Configs) SetUserConfigs(cfg *entity.UserConfig) error { - var rootCfg *entity.RootConfig - rootCfg, err := c.GetRootConfigs() - if err != nil { - rootCfg = &entity.RootConfig{} - } - - rootCfg.User = *cfg - - return c.SetRootConfig(rootCfg) -} diff --git a/constants/docs.go b/constants/docs.go deleted file mode 100644 index e00602b..0000000 --- a/constants/docs.go +++ /dev/null @@ -1,3 +0,0 @@ -package constants - -const RailwayDocsURL = "https://docs.railway.app" diff --git a/constants/url.go b/constants/url.go deleted file mode 100644 index 90409b3..0000000 --- a/constants/url.go +++ /dev/null @@ -1,5 +0,0 @@ -package constants - -const RailwayURLDefault = "https://railway.app" - -var RAILWAY_URL string = RailwayURLDefault diff --git a/constants/version.go b/constants/version.go deleted file mode 100644 index d8e85f2..0000000 --- a/constants/version.go +++ /dev/null @@ -1,9 +0,0 @@ -package constants - -const VersionDefault = "Piped into LDflags on build. You are probably running Railway CLI from source." - -var Version string = VersionDefault - -func IsDevVersion() bool { - return Version == VersionDefault -} diff --git a/controller/config.go b/controller/config.go deleted file mode 100644 index da77c4f..0000000 --- a/controller/config.go +++ /dev/null @@ -1,49 +0,0 @@ -package controller - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (c *Controller) GetProjectConfigs(ctx context.Context) (*entity.ProjectConfig, error) { - if c.cfg.RailwayProductionToken != "" { - // Get project config from api - projectToken, err := c.gtwy.GetProjectToken(ctx) - if err != nil { - return nil, err - } - - if projectToken != nil { - return &entity.ProjectConfig{ - Project: projectToken.ProjectId, - Environment: projectToken.EnvironmentId, - LockedEnvsNames: map[string]bool{}, - }, nil - } - } - - return c.cfg.GetProjectConfigs() -} - -func (c *Controller) PromptIfProtectedEnvironment(ctx context.Context) error { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return err - } - - if val, ok := projectCfg.LockedEnvsNames[projectCfg.Environment]; ok && val { - fmt.Println(ui.Bold(ui.RedText("Protected Environment Detected!").String())) - confirm, err := ui.PromptYesNo("Continue?") - if err != nil { - return err - } - if !confirm { - return nil - } - } - - return nil -} diff --git a/controller/deployment.go b/controller/deployment.go deleted file mode 100644 index 597cc4a..0000000 --- a/controller/deployment.go +++ /dev/null @@ -1,30 +0,0 @@ -package controller - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (c *Controller) GetDeployments(ctx context.Context) ([]*entity.Deployment, error) { - projectConfig, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - return c.gtwy.GetDeploymentsForEnvironment(ctx, projectConfig.Project, projectConfig.Environment) -} - -func (c *Controller) GetActiveDeployment(ctx context.Context) (*entity.Deployment, error) { - projectConfig, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - deployment, err := c.gtwy.GetLatestDeploymentForEnvironment(ctx, projectConfig.Project, projectConfig.Environment) - if err != nil { - return nil, err - } - - return deployment, nil -} diff --git a/controller/deployment_trigger.go b/controller/deployment_trigger.go deleted file mode 100644 index 32c844f..0000000 --- a/controller/deployment_trigger.go +++ /dev/null @@ -1,20 +0,0 @@ -package controller - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (c *Controller) DeployEnvironmentTriggers(ctx context.Context, serviceID *string) error { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return err - } - - return c.gtwy.DeployEnvironmentTriggers(ctx, &entity.DeployEnvironmentTriggersRequest{ - ProjectID: projectCfg.Project, - EnvironmentID: projectCfg.Environment, - ServiceID: *serviceID, - }) -} diff --git a/controller/down.go b/controller/down.go deleted file mode 100644 index cd5f088..0000000 --- a/controller/down.go +++ /dev/null @@ -1,12 +0,0 @@ -package controller - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (c *Controller) Down(ctx context.Context, req *entity.DownRequest) error { - err := c.gtwy.Down(ctx, req) - return err -} diff --git a/controller/environment.go b/controller/environment.go deleted file mode 100644 index bb281f3..0000000 --- a/controller/environment.go +++ /dev/null @@ -1,59 +0,0 @@ -package controller - -import ( - "context" - - "github.com/railwayapp/cli/entity" - CLIErrors "github.com/railwayapp/cli/errors" -) - -// GetCurrentEnvironment returns the currently active environment for the Railway project -func (c *Controller) GetCurrentEnvironment(ctx context.Context) (*entity.Environment, error) { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - project, err := c.GetProject(ctx, projectCfg.Project) - if err != nil { - return nil, err - } - - for _, environment := range project.Environments { - if environment.Id == projectCfg.Environment { - return environment, nil - } - } - return nil, CLIErrors.EnvironmentNotSet -} - -func (c *Controller) GetEnvironmentByName(ctx context.Context, environmentName string) (*entity.Environment, error) { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - project, err := c.GetProject(ctx, projectCfg.Project) - if err != nil { - return nil, err - } - - for _, environment := range project.Environments { - if environment.Name == environmentName { - return environment, nil - } - } - return nil, CLIErrors.EnvironmentNotFound -} - -func (c *Controller) CreateEnvironment(ctx context.Context, req *entity.CreateEnvironmentRequest) (*entity.Environment, error) { - return c.gtwy.CreateEnvironment(ctx, req) -} - -func (c *Controller) CreateEphemeralEnvironment(ctx context.Context, req *entity.CreateEphemeralEnvironmentRequest) (*entity.Environment, error) { - return c.gtwy.CreateEphemeralEnvironment(ctx, req) -} - -func (c *Controller) DeleteEnvironment(ctx context.Context, req *entity.DeleteEnvironmentRequest) error { - return c.gtwy.DeleteEnvironment(ctx, req) -} diff --git a/controller/envs.go b/controller/envs.go deleted file mode 100644 index 822f974..0000000 --- a/controller/envs.go +++ /dev/null @@ -1,265 +0,0 @@ -package controller - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "os" - - "github.com/joho/godotenv" - "github.com/railwayapp/cli/entity" - CLIErrors "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -func (c *Controller) GetEnvsForCurrentEnvironment(ctx context.Context, serviceName *string) (*entity.Envs, error) { - environment, err := c.GetCurrentEnvironment(ctx) - if err != nil { - return nil, err - } - - return c.GetEnvs(ctx, environment, serviceName) -} - -func (c *Controller) GetEnvs(ctx context.Context, environment *entity.Environment, serviceName *string) (*entity.Envs, error) { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - project, err := c.GetCurrentProject(ctx) - if err != nil { - return nil, err - } - - // Get service id from name - serviceId := "" - - if serviceName != nil && *serviceName != "" { - for _, service := range project.Services { - if service.Name == *serviceName { - serviceId = service.ID - } - } - - if serviceId == "" { - return nil, CLIErrors.ServiceNotFound - } - } - - if serviceId == "" { - service, err := ui.PromptServices(project.Services) - if err != nil { - return nil, err - } - - if service != nil { - serviceId = service.ID - } - } - - if val, ok := projectCfg.LockedEnvsNames[environment.Id]; ok && val { - fmt.Println(ui.Bold(ui.RedText("Protected Environment Detected!").String())) - confirm, err := ui.PromptYesNo("Continue fetching variables?") - if err != nil { - return nil, err - } - if !confirm { - return nil, nil - } - } - - return c.gtwy.GetEnvs(ctx, &entity.GetEnvsRequest{ - ProjectID: projectCfg.Project, - EnvironmentID: environment.Id, - ServiceID: serviceId, - }) -} - -func (c *Controller) AutoImportDotEnv(ctx context.Context) error { - dir, err := os.Getwd() - if err != nil { - return err - } - - envFileLocation := fmt.Sprintf("%s/.env", dir) - if _, err := os.Stat(envFileLocation); err == nil { - // path/to/whatever does not exist - shouldImportEnvs, err := ui.PromptYesNo("\n.env detected!\nImport your variables into Railway?") - if err != nil { - return err - } - // If the user doesn't want to import envs skip - if !shouldImportEnvs { - return nil - } - // Otherwise read .env and set envs - err = godotenv.Load() - if err != nil { - return err - } - envMap, err := godotenv.Read() - if err != nil { - return err - } - if len(envMap) > 0 { - return c.UpdateEnvs(ctx, (*entity.Envs)(&envMap), nil, false) - } - } - return nil -} - -func (c *Controller) SaveEnvsToFile(ctx context.Context) error { - envs, err := c.GetEnvsForCurrentEnvironment(ctx, nil) - if err != nil { - return err - } - - err = c.cfg.CreatePathIfNotExist(c.cfg.RailwayEnvFilePath) - if err != nil { - return err - } - - encoded, err := json.MarshalIndent(envs, "", " ") - if err != nil { - return err - } - - err = ioutil.WriteFile(c.cfg.RailwayEnvFilePath, encoded, os.ModePerm) - if err != nil { - return err - } - - return nil -} - -func (c *Controller) UpdateEnvs(ctx context.Context, envs *entity.Envs, serviceName *string, replace bool) error { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return err - } - - err = c.PromptIfProtectedEnvironment(ctx) - if err != nil { - return err - } - - project, err := c.GetProject(ctx, projectCfg.Project) - if err != nil { - return err - } - - // Get service id from name - serviceID := "" - if serviceName != nil && *serviceName != "" { - for _, service := range project.Services { - if service.Name == *serviceName { - serviceID = service.ID - } - } - - if serviceID == "" { - return CLIErrors.ServiceNotFound - } - } - - if serviceID == "" { - service, err := ui.PromptServices(project.Services) - if err != nil { - return err - } - if service != nil { - serviceID = service.ID - } - } - - pluginID := "" - - // If there is no service, use the env plugin - if serviceID == "" { - for _, p := range project.Plugins { - if p.Name == "env" { - pluginID = p.ID - } - } - } - - return c.gtwy.UpdateVariablesFromObject(ctx, &entity.UpdateEnvsRequest{ - ProjectID: projectCfg.Project, - EnvironmentID: projectCfg.Environment, - PluginID: pluginID, - ServiceID: serviceID, - Envs: envs, - Replace: replace, - }) -} - -func (c *Controller) DeleteEnvs(ctx context.Context, names []string, serviceName *string) error { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return err - } - - err = c.PromptIfProtectedEnvironment(ctx) - if err != nil { - return err - } - - project, err := c.GetProject(ctx, projectCfg.Project) - if err != nil { - return err - } - - // Get service id from name - serviceID := "" - if serviceName != nil && *serviceName != "" { - for _, service := range project.Services { - if service.Name == *serviceName { - serviceID = service.ID - } - } - - if serviceID == "" { - return CLIErrors.ServiceNotFound - } - } - - if serviceID == "" { - service, err := ui.PromptServices(project.Services) - if err != nil { - return err - } - if service != nil { - serviceID = service.ID - } - } - - pluginID := "" - - // If there is no service, use the env plugin - if serviceID == "" { - for _, p := range project.Plugins { - if p.Name == "env" { - pluginID = p.ID - } - } - } - - // Delete each variable one by one - for _, name := range names { - err = c.gtwy.DeleteVariable(ctx, &entity.DeleteVariableRequest{ - ProjectID: projectCfg.Project, - EnvironmentID: projectCfg.Environment, - PluginID: pluginID, - ServiceID: serviceID, - Name: name, - }) - - if err != nil { - return err - } - } - - return nil -} diff --git a/controller/logs.go b/controller/logs.go deleted file mode 100644 index 17b089e..0000000 --- a/controller/logs.go +++ /dev/null @@ -1,149 +0,0 @@ -package controller - -import ( - "context" - "errors" - "fmt" - "math" - "strings" - "time" - - "github.com/railwayapp/cli/entity" -) - -const ( - GQL_SOFT_ERROR = "Error fetching build logs" -) - -func (c *Controller) GetActiveDeploymentLogs(ctx context.Context, numLines int32) error { - deployment, err := c.GetActiveDeployment(ctx) - if err != nil { - return err - } - - return c.logsForState(ctx, &entity.DeploymentLogsRequest{ - DeploymentID: deployment.ID, - ProjectID: deployment.ProjectID, - NumLines: numLines, - }) -} - -func (c *Controller) GetActiveBuildLogs(ctx context.Context, numLines int32) error { - projectConfig, err := c.GetProjectConfigs(ctx) - if err != nil { - return err - } - - deployment, err := c.gtwy.GetLatestDeploymentForEnvironment(ctx, projectConfig.Project, projectConfig.Environment) - if err != nil { - return err - } - - return c.logsForState(ctx, &entity.DeploymentLogsRequest{ - DeploymentID: deployment.ID, - ProjectID: projectConfig.Project, - NumLines: numLines, - }) -} - -/* Logs for state will get logs for a current state (Either building or not building state) - It does this by capturing the initial state of the deploy, and looping while it stays in that state - The loop captures the previous deploy as well as the current and does log diffing on the unified state - When the state transitions from building to not building, the loop breaks -*/ -func (c *Controller) logsForState(ctx context.Context, req *entity.DeploymentLogsRequest) error { - // Stream on building -> Building until !Building then break - // Stream on not building -> !Building until Failed then break - deploy, err := c.gtwy.GetDeploymentByID(ctx, &entity.DeploymentByIDRequest{ - DeploymentID: req.DeploymentID, - ProjectID: req.ProjectID, - GQL: c.getQuery(ctx, ""), - }) - if err != nil { - return err - } - - // Print Logs w/ Limit - logLines := strings.Split(logsForState(ctx, deploy.Status, deploy), "\n") - offset := 0.0 - if req.NumLines != 0 { - // If a limit is set, walk it back n steps (with a min of zero so no panics) - offset = math.Max(float64(len(logLines))-float64(req.NumLines)-1, 0.0) - } - // GQL may return partial errors for build logs if not ready - // The response won't fail but will be a partial error. Check this. - err = errFromGQL(ctx, logLines) - if err != nil { - return err - } - - // Output Initial Logs - currLogs := strings.Join(logLines[int(offset):], "\n") - if len(currLogs) > 0 { - fmt.Println(currLogs) - } - - if deploy.Status == entity.STATUS_FAILED { - return errors.New("Build Failed! Please see output for more information") - } - - prevDeploy := deploy - logState := deploy.Status - deltaState := hasTransitioned(nil, deploy) - - for !deltaState && req.NumLines == 0 { - time.Sleep(2 * time.Second) - currDeploy, err := c.gtwy.GetDeploymentByID(ctx, &entity.DeploymentByIDRequest{ - DeploymentID: req.DeploymentID, - ProjectID: req.ProjectID, - GQL: c.getQuery(ctx, logState), - }) - if err != nil { - return err - } - // Current Logs fetched from server - currLogs := strings.Split(logsForState(ctx, logState, currDeploy), "\n") - // Previous logs fetched from prevDeploy variable - prevLogs := strings.Split(logsForState(ctx, logState, prevDeploy), "\n") - // Diff logs using the line numbers as references - logDiff := currLogs[len(prevLogs)-1 : len(currLogs)-1] - // If no changes we continue - if len(logDiff) == 0 { - continue - } - // Output logs - fmt.Println(strings.Join(logDiff, "\n")) - // Set out walk pointer forward using the newest logs - deltaState = hasTransitioned(prevDeploy, currDeploy) - prevDeploy = currDeploy - } - return nil -} - -func hasTransitioned(prev *entity.Deployment, curr *entity.Deployment) bool { - return prev != nil && curr != nil && prev.Status != curr.Status -} - -func (c *Controller) getQuery(ctx context.Context, status string) entity.DeploymentGQL { - return entity.DeploymentGQL{ - BuildLogs: status == entity.STATUS_BUILDING || status == "", - DeployLogs: status != entity.STATUS_BUILDING || status == "", - Status: true, - } -} - -func logsForState(ctx context.Context, status string, deploy *entity.Deployment) string { - if status == entity.STATUS_BUILDING { - return deploy.BuildLogs - } - return deploy.DeployLogs -} - -func errFromGQL(ctx context.Context, logLines []string) error { - for _, l := range logLines { - if strings.Contains(l, GQL_SOFT_ERROR) { - return errors.New(GQL_SOFT_ERROR) - } - } - return nil -} diff --git a/controller/main.go b/controller/main.go deleted file mode 100644 index 5089d0f..0000000 --- a/controller/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package controller - -import ( - "github.com/google/go-github/github" - "github.com/railwayapp/cli/configs" - "github.com/railwayapp/cli/gateway" - "github.com/railwayapp/cli/random" -) - -type Controller struct { - gtwy *gateway.Gateway - cfg *configs.Configs - randomizer *random.Randomizer - ghc *github.Client -} - -func New() *Controller { - return &Controller{ - gtwy: gateway.New(), - cfg: configs.New(), - randomizer: random.New(), - ghc: github.NewClient(nil), - } -} diff --git a/controller/panic.go b/controller/panic.go deleted file mode 100644 index cbbe706..0000000 --- a/controller/panic.go +++ /dev/null @@ -1,45 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "os" - - "github.com/railwayapp/cli/constants" - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" -) - -func (c *Controller) SendPanic(ctx context.Context, panicErr string, stacktrace string, command string) (bool, error) { - confirmSendPanic() - - projectCfg, err := c.cfg.GetProjectConfigs() - if err != nil { - return c.gtwy.SendPanic(ctx, &entity.PanicRequest{ - Command: command, - PanicError: panicErr, - Stacktrace: stacktrace, - ProjectID: "", - EnvironmentID: "", - Version: constants.Version, - }) - - } - return c.gtwy.SendPanic(ctx, &entity.PanicRequest{ - Command: command, - PanicError: panicErr, - Stacktrace: stacktrace, - ProjectID: projectCfg.Project, - EnvironmentID: projectCfg.Environment, - Version: constants.Version, - }) - -} - -func confirmSendPanic() { - fmt.Printf("🚨 Looks like something derailed, Press Enter to send error logs (^C to quit)") - fmt.Fscanln(os.Stdin) - ui.StartSpinner(&ui.SpinnerCfg{ - Message: "Taking notes...", - }) -} diff --git a/controller/plugin.go b/controller/plugin.go deleted file mode 100644 index 232caae..0000000 --- a/controller/plugin.go +++ /dev/null @@ -1,19 +0,0 @@ -package controller - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (c *Controller) CreatePlugin(ctx context.Context, req *entity.CreatePluginRequest) (*entity.Plugin, error) { - return c.gtwy.CreatePlugin(ctx, req) -} - -func (c *Controller) GetAvailablePlugins(ctx context.Context, projectId string) ([]string, error) { - plugins, err := c.gtwy.GetAvailablePlugins(ctx, projectId) - if err != nil { - return nil, err - } - return plugins, nil -} diff --git a/controller/project.go b/controller/project.go deleted file mode 100644 index 3ba51ec..0000000 --- a/controller/project.go +++ /dev/null @@ -1,135 +0,0 @@ -package controller - -import ( - "context" - CLIErrors "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" - - "github.com/railwayapp/cli/entity" -) - -// GetCurrentProject returns the currently active project -func (c *Controller) GetCurrentProject(ctx context.Context) (*entity.Project, error) { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - project, err := c.GetProject(ctx, projectCfg.Project) - if err != nil { - return nil, err - } - - return project, nil -} - -// GetProject returns a project of id projectId, error otherwise -func (c *Controller) GetProject(ctx context.Context, projectId string) (*entity.Project, error) { - return c.gtwy.GetProject(ctx, projectId) -} - -// GetProjectByName returns a project for the user of name projectName, error otherwise -func (c *Controller) GetProjectByName(ctx context.Context, projectName string) (*entity.Project, error) { - return c.gtwy.GetProjectByName(ctx, projectName) -} - -func (c *Controller) GetServiceIdByName(ctx context.Context, serviceName *string) (*string, error) { - projectCfg, err := c.GetProjectConfigs(ctx) - if err != nil { - return nil, err - } - - err = c.PromptIfProtectedEnvironment(ctx) - if err != nil { - return nil, err - } - - project, err := c.GetProject(ctx, projectCfg.Project) - if err != nil { - return nil, err - } - - // Get service id from name - serviceID := "" - if serviceName != nil && *serviceName != "" { - for _, service := range project.Services { - if service.Name == *serviceName { - serviceID = service.ID - } - } - - if serviceID == "" { - return nil, CLIErrors.ServiceNotFound - } - } - - if serviceID == "" { - service, err := ui.PromptServices(project.Services) - if err != nil { - return nil, err - } - if service != nil { - serviceID = service.ID - } - } - - return &serviceID, nil -} - -// CreateProject creates a project specified by the project request, error otherwise -func (c *Controller) CreateProject(ctx context.Context, req *entity.CreateProjectRequest) (*entity.Project, error) { - return c.gtwy.CreateProject(ctx, req) -} - -// CreateProjectFromTemplate creates a project from template specified by the project request, error otherwise -func (c *Controller) CreateProjectFromTemplate(ctx context.Context, req *entity.CreateProjectFromTemplateRequest) (*entity.CreateProjectFromTemplateResult, error) { - return c.gtwy.CreateProjectFromTemplate(ctx, req) -} - -// UpdateProject updates a project specified by the project request, error otherwise -func (c *Controller) UpdateProject(ctx context.Context, req *entity.UpdateProjectRequest) (*entity.Project, error) { - return c.gtwy.UpdateProject(ctx, req) -} - -// GetProjects returns all projects associated with the user, error otherwise -func (c *Controller) GetProjects(ctx context.Context) ([]*entity.Project, error) { - return c.gtwy.GetProjects(ctx) -} - -// OpenProjectInBrowser opens the provided projectId in the browser -func (c *Controller) OpenProjectInBrowser(ctx context.Context, projectID string, environmentID string) error { - return c.gtwy.OpenProjectInBrowser(projectID, environmentID) -} - -// OpenProjectPathInBrowser opens the provided projectId with the provided path in the browser -func (c *Controller) OpenProjectPathInBrowser(ctx context.Context, projectID string, environmentID string, path string) error { - return c.gtwy.OpenProjectPathInBrowser(projectID, environmentID, path) -} - -// OpenProjectDeploymentsInBrowser opens the provided projectId's depolyments in the browser -func (c *Controller) OpenProjectDeploymentsInBrowser(ctx context.Context, projectID string) error { - return c.gtwy.OpenProjectDeploymentsInBrowser(projectID) -} - -// GetProjectDeploymentsURL returns the URL to access project deployment in browser -func (c *Controller) GetProjectDeploymentsURL(ctx context.Context, projectID string) string { - return c.gtwy.GetProjectDeploymentsURL(projectID) -} - -// GetServiceDeploymentsURL returns the URL to access service deployments in the browser -func (c *Controller) GetServiceDeploymentsURL(ctx context.Context, projectID string, serviceID string, deploymentID string) string { - return c.gtwy.GetServiceDeploymentsURL(projectID, serviceID, deploymentID) -} - -// GetLatestDeploymentForEnvironment returns the URL to access project deployment in browser -func (c *Controller) GetLatestDeploymentForEnvironment(ctx context.Context, projectID string, environmentID string) (*entity.Deployment, error) { - return c.gtwy.GetLatestDeploymentForEnvironment(ctx, projectID, environmentID) -} - -func (c *Controller) OpenStaticUrlInBrowser(staticUrl string) error { - return c.gtwy.OpenStaticUrlInBrowser(staticUrl) -} - -func (c *Controller) DeleteProject(ctx context.Context, projectID string) error { - return c.gtwy.DeleteProject(ctx, projectID) -} diff --git a/controller/scope.go b/controller/scope.go deleted file mode 100644 index c37cd5f..0000000 --- a/controller/scope.go +++ /dev/null @@ -1,10 +0,0 @@ -package controller - -import ( - "context" -) - -// GetWritableGithubScopes creates a project specified by the project request, error otherwise -func (c *Controller) GetWritableGithubScopes(ctx context.Context) ([]string, error) { - return c.gtwy.GetWritableGithubScopes(ctx) -} diff --git a/controller/starter.go b/controller/starter.go deleted file mode 100644 index 0490d0a..0000000 --- a/controller/starter.go +++ /dev/null @@ -1,12 +0,0 @@ -package controller - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -// GetStarters returns all available starters -func (c *Controller) GetStarters(ctx context.Context) ([]*entity.Starter, error) { - return c.gtwy.GetStarters(ctx) -} diff --git a/controller/up.go b/controller/up.go deleted file mode 100644 index 895f6e8..0000000 --- a/controller/up.go +++ /dev/null @@ -1,209 +0,0 @@ -package controller - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "context" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - - "github.com/railwayapp/cli/entity" - gitignore "github.com/railwayapp/cli/gateway" -) - -var validIgnoreFile = map[string]bool{ - ".gitignore": true, - ".railwayignore": true, -} - -var skipDirs = []string{ - ".git", - "node_modules", -} - -type ignoreFile struct { - prefix string - ignore *gitignore.GitIgnore -} - -func scanIgnoreFiles(src string) ([]ignoreFile, error) { - ignoreFiles := []ignoreFile{} - - if err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - // no sense scanning for ignore files in skipped dirs - for _, s := range skipDirs { - if filepath.Base(path) == s { - return filepath.SkipDir - } - } - - return nil - } - - fname := filepath.Base(path) - if validIgnoreFile[fname] { - igf, err := gitignore.CompileIgnoreFile(path) - if err != nil { - return err - } - - prefix := filepath.Dir(path) - if prefix == "." { - prefix = "" // Handle root dir properly. - } - - ignoreFiles = append(ignoreFiles, ignoreFile{ - prefix: prefix, - ignore: igf, - }) - } - - return nil - }); err != nil { - return nil, err - } - - return ignoreFiles, nil -} - -func compress(src string, buf io.Writer) error { - // tar > gzip > buf - zr := gzip.NewWriter(buf) - tw := tar.NewWriter(zr) - - // find all ignore files, including those in subdirs - ignoreFiles, err := scanIgnoreFiles(src) - if err != nil { - return err - } - - // walk through every file in the folder - err = filepath.WalkDir(src, func(absoluteFile string, de os.DirEntry, passedErr error) error { - relativeFile, err := filepath.Rel(src, absoluteFile) - if err != nil { - return err - } - - if passedErr != nil { - return err - } - - // follow symlinks by default - resolvedFilePath, err := filepath.EvalSymlinks(absoluteFile) - if err != nil { - return err - } - - // get info about the file the link points at - fileInfo, err := os.Lstat(resolvedFilePath) - if err != nil { - return err - } - - if fileInfo.IsDir() { - // skip directories if we can (for perf) - // e.g., want to avoid walking node_modules dir - for _, s := range skipDirs { - if filepath.Base(relativeFile) == s { - return filepath.SkipDir - } - } - - return nil - } - - for _, ignoredFile := range ignoreFiles { - if strings.HasPrefix(absoluteFile, ignoredFile.prefix) { // if ignore file applicable - trimmed := strings.TrimPrefix(absoluteFile, ignoredFile.prefix) - if ignoredFile.ignore.MatchesPath(trimmed) { - return nil - } - } - } - - // read file into a buffer to prevent tar overwrites - data := bytes.NewBuffer(nil) - f, err := os.Open(resolvedFilePath) - if err != nil { - return err - } - _, err = io.Copy(data, f) - if err != nil { - return err - } - - // close the file to avoid hitting fd limit - if err := f.Close(); err != nil { - return err - } - - // generate tar headers - header, err := tar.FileInfoHeader(fileInfo, resolvedFilePath) - if err != nil { - return err - } - - // must provide real name - // (see https://golang.org/src/archive/tar/common.go?#L626) - header.Name = filepath.ToSlash(relativeFile) - // size when we first observed the file - header.Size = int64(data.Len()) - - // write header - if err := tw.WriteHeader(header); err != nil { - return err - } - // not a dir, write file content - if _, err := io.Copy(tw, data); err != nil { - return err - } - - return err - }) - - if err != nil { - return err - } - - // produce tar - if err := tw.Close(); err != nil { - return err - } - // produce gzip - if err := zr.Close(); err != nil { - return err - } - return nil -} - -func (c *Controller) Upload( - ctx context.Context, - req *entity.UploadRequest, -) (*entity.UpResponse, error) { - var buf bytes.Buffer - - if err := compress(req.RootDir, &buf); err != nil { - return nil, err - } - - return c.gtwy.Up(ctx, &entity.UpRequest{ - Data: buf, - ProjectID: req.ProjectID, - EnvironmentID: req.EnvironmentID, - ServiceID: req.ServiceID, - }) -} - -func (c *Controller) GetFullUrlFromStaticUrl(staticUrl string) string { - return fmt.Sprintf("https://%s", staticUrl) -} diff --git a/controller/user.go b/controller/user.go deleted file mode 100644 index edd147d..0000000 --- a/controller/user.go +++ /dev/null @@ -1,307 +0,0 @@ -package controller - -import ( - "context" - b64 "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "os" - "strconv" - "sync" - "time" - - "github.com/pkg/browser" - configs "github.com/railwayapp/cli/configs" - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" -) - -const ( - baseRailwayURL string = "https://railway.app" - baseStagingURL string = "https://railway-staging.app" - baseLocalhostURL string = "https://railway-develop.app" -) - -const ( - loginInvalidResponse string = "Invalid code" - loginSuccessResponse string = "Ok" -) - -type LoginResponse struct { - Status string `json:"status,omitempty"` - Error string `json:"error,omitempty"` -} - -const maxAttempts = 2 * 60 -const pollInterval = 1 * time.Second - -func (c *Controller) GetUser(ctx context.Context) (*entity.User, error) { - userCfg, err := c.cfg.GetUserConfigs() - if err != nil { - return nil, err - } - if userCfg.Token == "" { - return nil, errors.UserConfigNotFound - } - return c.gtwy.GetUser(ctx) -} - -func (c *Controller) browserBasedLogin(ctx context.Context) (*entity.User, error) { - var token string - var returnedCode string - port, err := c.randomizer.Port() - - if err != nil { - return nil, err - } - - code := c.randomizer.Code() - - wg := &sync.WaitGroup{} - wg.Add(1) - go func() { - ctx := context.Background() - srv := &http.Server{Addr: strconv.Itoa(port)} - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", getAPIURL()) - - if r.Method == http.MethodGet { - w.Header().Set("Content-Type", "application/json") - token = r.URL.Query().Get("token") - returnedCode = r.URL.Query().Get("code") - - if code != returnedCode { - res := LoginResponse{Error: loginInvalidResponse} - byteRes, err := json.Marshal(&res) - if err != nil { - fmt.Println(err) - } - w.WriteHeader(400) - _, err = w.Write(byteRes) - if err != nil { - fmt.Println("Invalid login response failed to serialize!") - } - return - } - - res := LoginResponse{Status: loginSuccessResponse} - byteRes, err := json.Marshal(&res) - - if err != nil { - fmt.Println(err) - } - w.WriteHeader(200) - _, err = w.Write(byteRes) - if err != nil { - fmt.Println("Valid login response failed to serialize!") - } - } else if r.Method == http.MethodOptions { - w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, PUT, PATCH, POST, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "*") - w.Header().Set("Content-Length", "0") - w.WriteHeader(204) - return - } - - wg.Done() - - if err := srv.Shutdown(ctx); err != nil { - fmt.Println(err) - } - }) - - if err := http.ListenAndServe(fmt.Sprintf("localhost:%d", port), nil); err != nil { - fmt.Println("Login server handshake failed!") - } - }() - - url := getBrowserBasedLoginURL(port, code) - err = c.ConfirmBrowserOpen("Logging in...", url) - - if err != nil { - // Opening the browser failed. Try browserless login - return c.browserlessLogin(ctx) - } - - fmt.Println("No dice? Try railway login --browserless") - - wg.Wait() - - if code != returnedCode { - return nil, errors.LoginFailed - } - - err = c.cfg.SetUserConfigs(&entity.UserConfig{ - Token: token, - }) - if err != nil { - return nil, err - } - - user, err := c.gtwy.GetUser(ctx) - if err != nil { - return nil, err - } - - return user, nil -} - -func (c *Controller) pollForToken(ctx context.Context, code string) (string, error) { - var count = 0 - for count < maxAttempts { - token, err := c.gtwy.ConsumeLoginSession(ctx, code) - - if err != nil { - return "", errors.LoginFailed - } - - if token != "" { - return token, nil - } - - count++ - time.Sleep(pollInterval) - } - - return "", errors.LoginTimeout -} - -func (c *Controller) browserlessLogin(ctx context.Context) (*entity.User, error) { - wordCode, err := c.gtwy.CreateLoginSession(ctx) - if err != nil { - return nil, err - } - - url := getBrowserlessLoginURL(wordCode) - - fmt.Printf("Your pairing code is: %s\n", ui.MagentaText(wordCode)) - fmt.Printf("To authenticate with Railway, please go to \n %s\n", url) - - token, err := c.pollForToken(ctx, wordCode) - if err != nil { - return nil, err - } - - err = c.cfg.SetUserConfigs(&entity.UserConfig{ - Token: token, - }) - if err != nil { - return nil, err - } - - user, err := c.gtwy.GetUser(ctx) - if err != nil { - return nil, err - } - - return user, nil -} - -func (c *Controller) Login(ctx context.Context, isBrowserless bool) (*entity.User, error) { - // Invalidate current session if it exists - if loggedIn, _ := c.IsLoggedIn(ctx); loggedIn { - if err := c.gtwy.Logout(ctx); err != nil { - return nil, err - } - } - - if isBrowserless || isSSH() || isCodeSpaces() { - return c.browserlessLogin(ctx) - } - - return c.browserBasedLogin(ctx) -} - -func (c *Controller) Logout(ctx context.Context) error { - if loggedIn, _ := c.IsLoggedIn(ctx); !loggedIn { - fmt.Printf("🚪 %s\n", ui.YellowText("Already logged out")) - return nil - } - - err := c.gtwy.Logout(ctx) - if err != nil { - return err - } - - err = c.cfg.SetUserConfigs(&entity.UserConfig{}) - if err != nil { - return err - } - - fmt.Printf("👋 %s\n", ui.YellowText("Logged out")) - return nil -} - -func (c *Controller) IsLoggedIn(ctx context.Context) (bool, error) { - userCfg, err := c.cfg.GetUserConfigs() - if err != nil { - return false, err - } - isLoggedIn := userCfg.Token != "" - return isLoggedIn, nil -} - -func (c *Controller) ConfirmBrowserOpen(spinnerMsg string, url string) error { - fmt.Printf("Press Enter to open the browser (^C to quit)") - fmt.Fscanln(os.Stdin) - ui.StartSpinner(&ui.SpinnerCfg{ - Message: spinnerMsg, - }) - - err := browser.OpenURL(url) - - if err != nil { - ui.StopSpinner("Failed to open browser, attempting browserless login.") - return err - } - - return nil -} - -func getAPIURL() string { - if configs.IsDevMode() { - return baseLocalhostURL - } - if configs.IsStagingMode() { - return baseStagingURL - } - return baseRailwayURL -} - -func getHostName() string { - name, err := os.Hostname() - if err != nil { - return "" - } - - return name -} - -func getBrowserBasedLoginURL(port int, code string) string { - hostname := getHostName() - buffer := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("port=%d&code=%s&hostname=%s", port, code, hostname))) - url := fmt.Sprintf("%s/cli-login?d=%s", getAPIURL(), buffer) - return url -} - -func getBrowserlessLoginURL(wordCode string) string { - hostname := getHostName() - buffer := b64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("wordCode=%s&hostname=%s", wordCode, hostname))) - - url := fmt.Sprintf("%s/cli-login?d=%s", getAPIURL(), buffer) - return url -} - -func isSSH() bool { - if os.Getenv("SSH_TTY") != "" || os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" { - return true - } - - return false -} - -func isCodeSpaces() bool { - return os.Getenv("CODESPACES") == "true" -} diff --git a/controller/version.go b/controller/version.go deleted file mode 100644 index 01c62f7..0000000 --- a/controller/version.go +++ /dev/null @@ -1,13 +0,0 @@ -package controller - -import ( - "context" -) - -func (c *Controller) GetLatestVersion() (string, error) { - rep, _, err := c.ghc.Repositories.GetLatestRelease(context.Background(), "railwayapp", "cli") - if err != nil { - return "", err - } - return *rep.TagName, nil -} diff --git a/controller/workflow.go b/controller/workflow.go deleted file mode 100644 index ddf4e55..0000000 --- a/controller/workflow.go +++ /dev/null @@ -1,11 +0,0 @@ -package controller - -import ( - "context" - "github.com/railwayapp/cli/entity" -) - -// GetWorkflowStatus fetches the status of a workflow based on request, error otherwise -func (c *Controller) GetWorkflowStatus(ctx context.Context, workflowID string) (entity.WorkflowStatus, error) { - return c.gtwy.GetWorkflowStatus(ctx, workflowID) -} diff --git a/entity/cobra.go b/entity/cobra.go deleted file mode 100644 index b252ff7..0000000 --- a/entity/cobra.go +++ /dev/null @@ -1,10 +0,0 @@ -package entity - -import "github.com/spf13/cobra" - -type CommandRequest struct { - Cmd *cobra.Command - Args []string -} - -type CobraFunction func(cmd *cobra.Command, args []string) error diff --git a/entity/config.go b/entity/config.go deleted file mode 100644 index 4368294..0000000 --- a/entity/config.go +++ /dev/null @@ -1,17 +0,0 @@ -package entity - -type RootConfig struct { - User UserConfig `json:"user"` - Projects map[string]ProjectConfig `json:"projects"` -} - -type UserConfig struct { - Token string `json:"token"` -} - -type ProjectConfig struct { - ProjectPath string `json:"projectPath,omitempty"` - Project string `json:"project,omitempty"` - Environment string `json:"environment,omitempty"` - LockedEnvsNames map[string]bool `json:"lockedEnvsNames,omitempty"` -} diff --git a/entity/deployment.go b/entity/deployment.go deleted file mode 100644 index aebe9af..0000000 --- a/entity/deployment.go +++ /dev/null @@ -1,45 +0,0 @@ -package entity - -const ( - STATUS_BUILDING = "BUILDING" - STATUS_DEPLOYING = "DEPLOYING" - STATUS_SUCCESS = "SUCCESS" - STATUS_REMOVED = "REMOVED" - STATUS_FAILED = "FAILED" -) - -type DeploymentMeta struct { - Repo string `json:"repo"` - Branch string `json:"branch"` - CommitHash string `json:"commitHash"` - CommitMessage string `json:"commitMessage"` -} - -type Deployment struct { - ID string `json:"id"` - ProjectID string `json:"projectId"` - BuildLogs string `json:"buildLogs"` - DeployLogs string `json:"deployLogs"` - Status string `json:"status"` - StaticUrl string `json:"staticUrl"` - Meta *DeploymentMeta `json:"meta"` -} - -type DeploymentLogsRequest struct { - ProjectID string `json:"projectId"` - DeploymentID string `json:"deploymentId"` - NumLines int32 `json:"numLines"` -} - -type DeploymentGQL struct { - ID bool `json:"id"` - BuildLogs bool `json:"buildLogs"` - DeployLogs bool `json:"deployLogs"` - Status bool `json:"status"` -} - -type DeploymentByIDRequest struct { - ProjectID string `json:"projectId"` - DeploymentID string `json:"deploymentId"` - GQL DeploymentGQL -} diff --git a/entity/deployment_trigger.go b/entity/deployment_trigger.go deleted file mode 100644 index 4459610..0000000 --- a/entity/deployment_trigger.go +++ /dev/null @@ -1,7 +0,0 @@ -package entity - -type DeployEnvironmentTriggersRequest struct { - ProjectID string - EnvironmentID string - ServiceID string -} diff --git a/entity/down.go b/entity/down.go deleted file mode 100644 index 6f22ff1..0000000 --- a/entity/down.go +++ /dev/null @@ -1,6 +0,0 @@ -package entity - -type DownRequest struct { - ProjectID string - EnvironmentID string -} diff --git a/entity/environment.go b/entity/environment.go deleted file mode 100644 index 31fc5b1..0000000 --- a/entity/environment.go +++ /dev/null @@ -1,22 +0,0 @@ -package entity - -type Environment struct { - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` -} - -type CreateEnvironmentRequest struct { - Name string `json:"name,omitempty"` - ProjectID string `json:"projectId,omitempty"` -} - -type CreateEphemeralEnvironmentRequest struct { - Name string `json:"name,omitempty"` - ProjectID string `json:"projectId,omitempty"` - BaseEnvironmentID string `json:"baseEnvironmentId"` -} - -type DeleteEnvironmentRequest struct { - EnvironmentId string `json:"environmentId,omitempty"` - ProjectID string `json:"projectId,omitempty"` -} diff --git a/entity/envs.go b/entity/envs.go deleted file mode 100644 index d285ef3..0000000 --- a/entity/envs.go +++ /dev/null @@ -1,49 +0,0 @@ -package entity - -type GetEnvsRequest struct { - ProjectID string - EnvironmentID string - ServiceID string -} - -type GetEnvsForPluginRequest struct { - ProjectID string - EnvironmentID string - PluginID string -} - -type UpdateEnvsRequest struct { - ProjectID string - EnvironmentID string - PluginID string - ServiceID string - Envs *Envs - Replace bool -} - -type DeleteVariableRequest struct { - ProjectID string - EnvironmentID string - PluginID string - ServiceID string - Name string -} - -type Envs map[string]string - -func (e Envs) Get(name string) string { - return e[name] -} - -func (e Envs) Set(name, value string) { - e[name] = value -} - -func (e Envs) Has(name string) bool { - _, ok := e[name] - return ok -} - -func (e Envs) Delete(name string) { - delete(e, name) -} diff --git a/entity/handler.go b/entity/handler.go deleted file mode 100644 index b8a2e70..0000000 --- a/entity/handler.go +++ /dev/null @@ -1,7 +0,0 @@ -package entity - -import "context" - -type HandlerFunction func(context.Context, *CommandRequest) error - -type PanicFunction func(context.Context, string, string, string, []string) error diff --git a/entity/panic.go b/entity/panic.go deleted file mode 100644 index 68bd59a..0000000 --- a/entity/panic.go +++ /dev/null @@ -1,10 +0,0 @@ -package entity - -type PanicRequest struct { - Command string - PanicError string - Stacktrace string - ProjectID string - EnvironmentID string - Version string -} diff --git a/entity/plugin.go b/entity/plugin.go deleted file mode 100644 index 78a687a..0000000 --- a/entity/plugin.go +++ /dev/null @@ -1,15 +0,0 @@ -package entity - -type PluginList struct { - Plugins []*Plugin `json:"plugins,omitempty"` -} - -type Plugin struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` -} - -type CreatePluginRequest struct { - ProjectID string - Plugin string -} diff --git a/entity/project.go b/entity/project.go deleted file mode 100644 index f6c185b..0000000 --- a/entity/project.go +++ /dev/null @@ -1,42 +0,0 @@ -package entity - -type CreateProjectRequest struct { - Name *string // Optional - Description *string // Optional - Plugins []string // Optional -} - -type CreateProjectFromTemplateRequest struct { - Name string // Required - Owner string // Required - Template string // Required - IsPrivate bool // Optional - Plugins []string // Optional - Variables map[string]string // Optional -} - -type UpdateProjectRequest struct { - Id string // Required - Name *string // Optional - Description *string // Optional -} - -type CreateProjectFromTemplateResult struct { - WorkflowID string - ProjectID string -} - -type Project struct { - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` - Environments []*Environment `json:"environments,omitempty"` - Plugins []*Plugin `json:"plugins,omitempty"` - Team *string `json:"team,omitempty"` - Services []*Service `json:"services,omitempty"` -} - -type ProjectToken struct { - ProjectId string `json:"projectId"` - EnvironmentId string `json:"environmentId"` -} diff --git a/entity/service.go b/entity/service.go deleted file mode 100644 index 4c18eb5..0000000 --- a/entity/service.go +++ /dev/null @@ -1,6 +0,0 @@ -package entity - -type Service struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` -} diff --git a/entity/starter.go b/entity/starter.go deleted file mode 100644 index ac168e1..0000000 --- a/entity/starter.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -type Starter struct { - Title string `json:"title"` - Url string `json:"url"` - Source string `json:"source"` -} - -type StarterEnvVar struct { - Name string - Desc string - Default string - Optional bool -} diff --git a/entity/up.go b/entity/up.go deleted file mode 100644 index 51da314..0000000 --- a/entity/up.go +++ /dev/null @@ -1,27 +0,0 @@ -package entity - -import "bytes" - -type UploadRequest struct { - ProjectID string - EnvironmentID string - ServiceID string - RootDir string -} - -type UpRequest struct { - Data bytes.Buffer - ProjectID string - EnvironmentID string - ServiceID string -} - -type UpResponse struct { - URL string - DeploymentDomain string -} - -type UpErrorResponse struct { - Message string `json:"message"` - RequestID string `json:"reqId"` -} diff --git a/entity/user.go b/entity/user.go deleted file mode 100644 index c47430e..0000000 --- a/entity/user.go +++ /dev/null @@ -1,8 +0,0 @@ -package entity - -type User struct { - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Has2FA bool -} diff --git a/entity/workflow.go b/entity/workflow.go deleted file mode 100644 index f1b8089..0000000 --- a/entity/workflow.go +++ /dev/null @@ -1,25 +0,0 @@ -package entity - -type WorkflowStatus string - -func (s WorkflowStatus) IsError() bool { - return s == "Error" -} - -func (s WorkflowStatus) IsRunning() bool { - return s == "Running" -} - -func (s WorkflowStatus) IsComplete() bool { - return s == "Complete" -} - -var ( - WorkflowRunning WorkflowStatus = "Running" - WorkflowComplete WorkflowStatus = "Complete" - WorkflowError WorkflowStatus = "Error" -) - -type WorkflowStatusResponse struct { - Status WorkflowStatus `json:"status"` -} diff --git a/errors/main.go b/errors/main.go deleted file mode 100644 index 6c78eec..0000000 --- a/errors/main.go +++ /dev/null @@ -1,40 +0,0 @@ -package errors - -import ( - "fmt" - - "github.com/railwayapp/cli/ui" -) - -type RailwayError error - -// TEST - -var ( - RootConfigNotFound RailwayError = fmt.Errorf("Run %s to get started", ui.Bold("railway login")) - UserConfigNotFound RailwayError = fmt.Errorf("%s\nRun %s", ui.RedText("Not logged in."), ui.Bold("railway login")) - ProjectConfigNotFound RailwayError = fmt.Errorf("%s\nRun %s to create a new project, or %s to use an existing project", ui.RedText("Project not found"), ui.Bold("railway init"), ui.Bold("railway link")) - UserNotAuthorized RailwayError = fmt.Errorf("%s\nTry running %s", ui.RedText("Not authorized!"), ui.Bold("railway login")) - ProjectTokenNotFound RailwayError = fmt.Errorf("%s\n", ui.RedText("Project token not found")) - ProblemFetchingProjects RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem fetching your projects.")) - ProblemFetchingWritableGithubScopes RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem fetching GitHub metadata.")) - ProjectCreateFailed RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem creating the project.")) - ProjectCreateFromTemplateFailed RailwayError = fmt.Errorf("%s\nOne of our trains probably derailed!", ui.RedText("There was a problem creating the project from template.")) - ProductionTokenNotSet RailwayError = fmt.Errorf("%s\nRun %s and head under `tokens` section. You can generate tokens to access Railway environment variables. Set that token in your environment as `RAILWAY_TOKEN=` and you're all aboard!", ui.RedText("RAILWAY_TOKEN environment variable not set."), ui.Bold("railway open")) - EnvironmentNotSet RailwayError = fmt.Errorf("%s", ui.RedText("No active environment found. Please select one")) - EnvironmentNotFound RailwayError = fmt.Errorf("%s", ui.RedText("Environment does not exist on project. Specify an existing environment")) - NoGitHubScopesFound RailwayError = fmt.Errorf("%s", ui.RedText("No GitHub organizations found. Please link your GitHub account to Railway and try again.")) - CommandNotSpecified RailwayError = fmt.Errorf("%s\nRun %s", ui.RedText("Specify a command to run inside the railway environment. Not providing a command will build and run the Dockerfile in the current directory."), ui.Bold("railway run [cmd]")) - LoginFailed RailwayError = fmt.Errorf("%s", ui.RedText("Login failed")) - LoginTimeout RailwayError = fmt.Errorf("%s", ui.RedText("Login timeout")) - PluginAlreadyExists RailwayError = fmt.Errorf("%s", ui.RedText("Plugin already exists")) - PluginNotSpecified RailwayError = fmt.Errorf("%s\nRun %s", ui.RedText("Specify a plugin to create."), ui.Bold("railway add ")) - PluginCreateFailed RailwayError = fmt.Errorf("%s\nUhh Ohh! One of our trains derailed.", ui.RedText("There was a problem creating the plugin.")) - PluginGetFailed RailwayError = fmt.Errorf("%s\nUhh Ohh! One of our trains derailed.", ui.RedText("There was a problem getting plugins available for creation.")) - TelemetryFailed RailwayError = fmt.Errorf("%s", ui.RedText("One of our trains derailed. Any chance you can report this error on our Discord (https://railway.app/help)?")) - WorkflowFailed RailwayError = fmt.Errorf("%s", ui.RedText("There was a problem deploying the project. Any chance you can report this error on our Discord (https://railway.app/help)?")) - NoDeploymentsFound RailwayError = fmt.Errorf("%s", ui.RedText("No Deployments Found!")) - DeploymentFetchingFailed RailwayError = fmt.Errorf("%s", "Failed to fetch deployments") - CreateEnvironmentFailed RailwayError = fmt.Errorf("%s", ui.RedText("Creating environment failed!")) - ServiceNotFound RailwayError = fmt.Errorf("%s", ui.RedText("Service not found in project")) -) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ae21787 --- /dev/null +++ b/flake.lock @@ -0,0 +1,191 @@ +{ + "nodes": { + "advisory-db": { + "flake": false, + "locked": { + "lastModified": 1677345087, + "narHash": "sha256-PSkBGJ6KyGbTeLtEgdHSljm76NOeoww9hDgBD/QBffk=", + "owner": "rustsec", + "repo": "advisory-db", + "rev": "9a5b1008028e4b37e91f5951e639ad7848232f8e", + "type": "github" + }, + "original": { + "owner": "rustsec", + "repo": "advisory-db", + "type": "github" + } + }, + "crane": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1677370089, + "narHash": "sha256-sdZ3ull2bldQYXK7tsX097C8JEuhBGIFPSfmA5nVoXE=", + "owner": "ipetkov", + "repo": "crane", + "rev": "685e7494b02c6ac505482b1a471de4c285e87543", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1669833724, + "narHash": "sha256-/HEZNyGbnQecrgJnfE8d0WC5c1xuPSD2LUpB6YXlg4c=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4d2b37a84fad1091b9de401eb450aae66f1a741e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1665296151, + "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "advisory-db": "advisory-db", + "crane": "crane", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay_2" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "crane", + "flake-utils" + ], + "nixpkgs": [ + "crane", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1676437770, + "narHash": "sha256-mhJye91Bn0jJIE7NnEywGty/U5qdELfsT8S+FBjTdG4=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a619538647bd03e3ee1d7b947f7c11ff289b376e", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "flake-utils": "flake-utils_3", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1677465082, + "narHash": "sha256-b82PmPWkt0pAsxmc477Yowq1Ez1VyjA5wnxE+yoIOWg=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "2924bfce2fadc1ded4a2b8cfce7f2fd4ef41c36f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e07b0bf --- /dev/null +++ b/flake.nix @@ -0,0 +1,122 @@ +{ + description = "Interact with Railway via CLI"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/22.11"; + rust-overlay.url = "github:oxalica/rust-overlay"; + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + flake-utils.url = "github:numtide/flake-utils"; + + advisory-db = { + url = "github:rustsec/advisory-db"; + flake = false; + }; + }; + + outputs = { self, rust-overlay, nixpkgs, crane, flake-utils, advisory-db, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ rust-overlay.overlays.default ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + toolchain = pkgs.rust-bin.stable.latest.default; + + inherit (pkgs) lib; + + craneLib = (crane.mkLib pkgs).overrideToolchain toolchain; + src = + let + # Only keeps graphql files + markdownFilter = path: _type: builtins.match ".*graphql$" path != null; + markdownOrCargo = path: type: + (markdownFilter path type) || (craneLib.filterCargoSources path type); + in + lib.cleanSourceWith { + src = ./.; + filter = markdownOrCargo; + }; + + # Common arguments can be set here to avoid repeating them later + commonArgs = { + inherit src; + pname = "railway"; + buildInputs = [ + # Add additional build inputs here + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + # Additional darwin specific inputs can be set here + pkgs.libiconv + pkgs.darwin.apple_sdk.frameworks.Security + ]; + + # Additional environment variables can be set directly + # MY_CUSTOM_VAR = "some value"; + }; + + # Build *just* the cargo dependencies, so we can reuse + # all of that work (e.g. via cachix) when running in CI + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + # Build the actual crate itself, reusing the dependency + # artifacts from above. + railway = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + }); + + clippy = craneLib.cargoClippy (commonArgs // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + }); + + audit = craneLib.cargoAudit (commonArgs // { + inherit advisory-db; + }); + + fmt = craneLib.cargoFmt (commonArgs // { }); + + test = craneLib.cargoNextest (commonArgs // { + inherit cargoArtifacts; + partitions = 1; + partitionType = "count"; + }); + in + { + checks = { }; + + packages = { + default = railway; + inherit clippy audit fmt test; + }; + + apps = { + default = flake-utils.lib.mkApp { + drv = railway; + }; + + clippy = flake-utils.lib.mkApp { + drv = clippy; + }; + + audit = flake-utils.lib.mkApp { + drv = audit; + }; + + fmt = flake-utils.lib.mkApp { + drv = fmt; + }; + + test = flake-utils.lib.mkApp { + drv = test; + }; + }; + + devShells.default = + import ./shell.nix { + inherit pkgs; + }; + }); +} diff --git a/gateway/deployment.go b/gateway/deployment.go deleted file mode 100644 index c1cde2e..0000000 --- a/gateway/deployment.go +++ /dev/null @@ -1,82 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" - gqlgen "github.com/railwayapp/cli/lib/gql" -) - -func (g *Gateway) GetDeploymentsForEnvironment(ctx context.Context, projectId, environmentId string) ([]*entity.Deployment, error) { - gqlReq, err := g.NewRequestWithAuth(` - query ($projectId: ID!, $environmentId: ID!) { - allDeploymentsForEnvironment(projectId: $projectId, environmentId: $environmentId) { - id - status - projectId - meta - staticUrl - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", projectId) - gqlReq.Var("environmentId", environmentId) - - var resp struct { - Deployments []*entity.Deployment `json:"allDeploymentsForEnvironment"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.DeploymentFetchingFailed - } - return resp.Deployments, nil -} - -func (g *Gateway) GetLatestDeploymentForEnvironment(ctx context.Context, projectID, environmentID string) (*entity.Deployment, error) { - deployments, err := g.GetDeploymentsForEnvironment(ctx, projectID, environmentID) - if err != nil { - return nil, err - } - if len(deployments) == 0 { - return nil, errors.NoDeploymentsFound - } - for _, deploy := range deployments { - if deploy.Status != entity.STATUS_REMOVED { - return deploy, nil - } - } - return nil, errors.NoDeploymentsFound -} - -func (g *Gateway) GetDeploymentByID(ctx context.Context, req *entity.DeploymentByIDRequest) (*entity.Deployment, error) { - gen, err := gqlgen.AsGQL(ctx, req.GQL) - if err != nil { - return nil, err - } - gqlReq, err := g.NewRequestWithAuth(fmt.Sprintf(` - query ($projectId: ID!, $deploymentId: ID!) { - deploymentById(projectId: $projectId, deploymentId: $deploymentId) { - %s - } - } - `, *gen)) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("deploymentId", req.DeploymentID) - - var resp struct { - Deployment *entity.Deployment `json:"deploymentById"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.DeploymentFetchingFailed - } - return resp.Deployment, nil -} diff --git a/gateway/deployment_trigger.go b/gateway/deployment_trigger.go deleted file mode 100644 index 205b7ce..0000000 --- a/gateway/deployment_trigger.go +++ /dev/null @@ -1,32 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (g *Gateway) DeployEnvironmentTriggers(ctx context.Context, req *entity.DeployEnvironmentTriggersRequest) error { - gqlReq, err := g.NewRequestWithAuth(` - mutation($projectId: ID!, $environmentId: ID!, $serviceId: ID!) { - deployEnvironmentTriggers(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId) - } - `) - if err != nil { - return err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("environmentId", req.EnvironmentID) - gqlReq.Var("serviceId", req.ServiceID) - - var resp struct { - // Nothing useful here - } - - if err := gqlReq.Run(ctx, &resp); err != nil { - return err - } - - return nil -} diff --git a/gateway/down.go b/gateway/down.go deleted file mode 100644 index 9f947b5..0000000 --- a/gateway/down.go +++ /dev/null @@ -1,34 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (g *Gateway) Down(ctx context.Context, req *entity.DownRequest) error { - deployment, err := g.GetLatestDeploymentForEnvironment(ctx, req.ProjectID, req.EnvironmentID) - - if err != nil { - return err - } - - gqlReq, err := g.NewRequestWithAuth(` - mutation removeDeployment($projectId: ID!, $deploymentId: ID!) { - removeDeployment(projectId: $projectId, deploymentId: $deploymentId) - } - `) - - if err != nil { - return err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("deploymentId", deployment.ID) - - if err = gqlReq.Run(ctx, nil); err != nil { - return err - } - - return nil -} diff --git a/gateway/environment.go b/gateway/environment.go deleted file mode 100644 index 2a145ae..0000000 --- a/gateway/environment.go +++ /dev/null @@ -1,81 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -func (g *Gateway) CreateEnvironment(ctx context.Context, req *entity.CreateEnvironmentRequest) (*entity.Environment, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($name: String!, $projectId: String!) { - createEnvironment(name: $name, projectId: $projectId) { - id - name - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("name", req.Name) - - var resp struct { - Environment *entity.Environment `json:"createEnvironment,omitempty"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.CreateEnvironmentFailed - } - return resp.Environment, nil -} - -func (g *Gateway) CreateEphemeralEnvironment(ctx context.Context, req *entity.CreateEphemeralEnvironmentRequest) (*entity.Environment, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($name: String!, $projectId: String!, $baseEnvironmentId: String!) { - createEphemeralEnvironment(name: $name, projectId: $projectId, baseEnvironmentId: $baseEnvironmentId) { - id - name - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("name", req.Name) - gqlReq.Var("baseEnvironmentId", req.BaseEnvironmentID) - - var resp struct { - Environment *entity.Environment `json:"createEphemeralEnvironment,omitempty"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.CreateEnvironmentFailed - } - return resp.Environment, nil -} - -func (g *Gateway) DeleteEnvironment(ctx context.Context, req *entity.DeleteEnvironmentRequest) error { - gqlReq, err := g.NewRequestWithAuth(` - mutation($environmentId: String!, $projectId: String!) { - deleteEnvironment(environmentId: $environmentId, projectId: $projectId) - } - `) - if err != nil { - return err - } - - gqlReq.Var("environmentId", req.EnvironmentId) - gqlReq.Var("projectId", req.ProjectID) - - var resp struct { - Created bool `json:"createEnvironment,omitempty"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return errors.CreateEnvironmentFailed - } - return nil -} diff --git a/gateway/envs.go b/gateway/envs.go deleted file mode 100644 index d20c8d6..0000000 --- a/gateway/envs.go +++ /dev/null @@ -1,96 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - - "github.com/railwayapp/cli/entity" -) - -func (g *Gateway) GetEnvs(ctx context.Context, req *entity.GetEnvsRequest) (*entity.Envs, error) { - gqlReq, err := g.NewRequestWithAuth(` - query ($projectId: String!, $environmentId: String!, $serviceId: String!) { - decryptedVariablesForService(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId) - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("environmentId", req.EnvironmentID) - - if req.ServiceID != "" { - gqlReq.Var("serviceId", req.ServiceID) - } - - var resp struct { - Envs *entity.Envs `json:"decryptedVariablesForService"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, err - } - return resp.Envs, nil -} - -func (g *Gateway) UpdateVariablesFromObject(ctx context.Context, req *entity.UpdateEnvsRequest) error { - queryName := "upsertVariablesFromObject" - - if req.Replace { - // When replacing, use the set query which will blow away all old variables and only set the ones in this query - queryName = "variablesSetFromObject" - } - - gqlReq, err := g.NewRequestWithAuth(fmt.Sprintf(` - mutation($projectId: String!, $environmentId: String!, $pluginId: String, $serviceId: String, $variables: Json!) { - %s(projectId: $projectId, environmentId: $environmentId, pluginId: $pluginId, serviceId: $serviceId, variables: $variables) - } - `, queryName)) - if err != nil { - return err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("environmentId", req.EnvironmentID) - if req.PluginID != "" { - gqlReq.Var("pluginId", req.PluginID) - } - if req.ServiceID != "" { - gqlReq.Var("serviceId", req.ServiceID) - } - - gqlReq.Var("variables", req.Envs) - - if err := gqlReq.Run(ctx, nil); err != nil { - return err - } - - return nil -} - -func (g *Gateway) DeleteVariable(ctx context.Context, req *entity.DeleteVariableRequest) error { - gqlReq, err := g.NewRequestWithAuth(` - mutation($projectId: String!, $environmentId: String!, $pluginId: String, $serviceId: String, $name: String!) { - deleteVariable(projectId: $projectId, environmentId: $environmentId, pluginId: $pluginId, serviceId: $serviceId, name: $name) - } - `) - if err != nil { - return err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("environmentId", req.EnvironmentID) - gqlReq.Var("name", req.Name) - if req.PluginID != "" { - gqlReq.Var("pluginId", req.PluginID) - } - if req.ServiceID != "" { - gqlReq.Var("serviceId", req.ServiceID) - } - - if err := gqlReq.Run(ctx, nil); err != nil { - return err - } - - return nil -} diff --git a/gateway/gitignore.go b/gateway/gitignore.go deleted file mode 100644 index 41a448e..0000000 --- a/gateway/gitignore.go +++ /dev/null @@ -1,224 +0,0 @@ -/* -ignore is a library which returns a new ignorer object which can -test against various paths. This is particularly useful when trying -to filter files based on a .gitignore document - -The rules for parsing the input file are the same as the ones listed -in the Git docs here: http://git-scm.com/docs/gitignore - -The summarized version of the same has been copied here: - - 1. A blank line matches no files, so it can serve as a separator - for readability. - 2. A line starting with # serves as a comment. Put a backslash ("\") - in front of the first hash for patterns that begin with a hash. - 3. Trailing spaces are ignored unless they are quoted with backslash ("\"). - 4. An optional prefix "!" which negates the pattern; any matching file - excluded by a previous pattern will become included again. It is not - possible to re-include a file if a parent directory of that file is - excluded. Git doesn’t list excluded directories for performance reasons, - so any patterns on contained files have no effect, no matter where they - are defined. Put a backslash ("\") in front of the first "!" for - patterns that begin with a literal "!", for example, "\!important!.txt". - 5. If the pattern ends with a slash, it is removed for the purpose of the - following description, but it would only find a match with a directory. - In other words, foo/ will match a directory foo and paths underneath it, - but will not match a regular file or a symbolic link foo (this is - consistent with the way how pathspec works in general in Git). - 6. If the pattern does not contain a slash /, Git treats it as a shell glob - pattern and checks for a match against the pathname relative to the - location of the .gitignore file (relative to the toplevel of the work - tree if not from a .gitignore file). - 7. Otherwise, Git treats the pattern as a shell glob suitable for - consumption by fnmatch(3) with the FNM_PATHNAME flag: wildcards in the - pattern will not match a / in the pathname. For example, - "Documentation/*.html" matches "Documentation/git.html" but not - "Documentation/ppc/ppc.html" or "tools/perf/Documentation/perf.html". - 8. A leading slash matches the beginning of the pathname. For example, - "/*.c" matches "cat-file.c" but not "mozilla-sha1/sha1.c". - 9. Two consecutive asterisks ("**") in patterns matched against full - pathname may have special meaning: - i. A leading "**" followed by a slash means match in all directories. - For example, "** /foo" matches file or directory "foo" anywhere, - the same as pattern "foo". "** /foo/bar" matches file or directory - "bar" anywhere that is directly under directory "foo". - ii. A trailing "/**" matches everything inside. For example, "abc/**" - matches all files inside directory "abc", relative to the location - of the .gitignore file, with infinite depth. - iii. A slash followed by two consecutive asterisks then a slash matches - zero or more directories. For example, "a/** /b" matches "a/b", - "a/x/b", "a/x/y/b" and so on. - iv. Other consecutive asterisks are considered invalid. */ -package gateway - -import ( - "io/ioutil" - "os" - "regexp" - "strings" -) - -//////////////////////////////////////////////////////////// - -// An IgnoreParser is an interface which exposes a single method: -// MatchesPath() - Returns true if the path is targeted by the patterns compiled -// in the GitIgnore structure -type IgnoreParser interface { - MatchesPath(f string) bool -} - -//////////////////////////////////////////////////////////// - -// This function pretty much attempts to mimic the parsing rules -// listed above at the start of this file -func getPatternFromLine(line string) (*regexp.Regexp, bool) { - // Trim OS-specific carriage returns. - line = strings.TrimRight(line, "\r") - - // Strip comments [Rule 2] - if strings.HasPrefix(line, `#`) { - return nil, false - } - - // Trim string [Rule 3] - // TODO: Handle [Rule 3], when the " " is escaped with a \ - line = strings.Trim(line, " ") - - // Exit for no-ops and return nil which will prevent us from - // appending a pattern against this line - if line == "" { - return nil, false - } - - // TODO: Handle [Rule 4] which negates the match for patterns leading with "!" - negatePattern := false - if line[0] == '!' { - negatePattern = true - line = line[1:] - } - - // Handle [Rule 2, 4], when # or ! is escaped with a \ - // Handle [Rule 4] once we tag negatePattern, strip the leading ! char - if regexp.MustCompile(`^(\#|\!)`).MatchString(line) { - line = line[1:] - } - - // If we encounter a foo/*.blah in a folder, prepend the / char - if regexp.MustCompile(`([^\/+])/.*\*\.`).MatchString(line) && line[0] != '/' { - line = "/" + line - } - - // Handle escaping the "." char - line = regexp.MustCompile(`\.`).ReplaceAllString(line, `\.`) - - magicStar := "#$~" - - // Handle "/**/" usage - if strings.HasPrefix(line, "/**/") { - line = line[1:] - } - line = regexp.MustCompile(`/\*\*/`).ReplaceAllString(line, `(/|/.+/)`) - line = regexp.MustCompile(`\*\*/`).ReplaceAllString(line, `(|.`+magicStar+`/)`) - line = regexp.MustCompile(`/\*\*`).ReplaceAllString(line, `(|/.`+magicStar+`)`) - - // Handle escaping the "*" char - line = regexp.MustCompile(`\\\*`).ReplaceAllString(line, `\`+magicStar) - line = regexp.MustCompile(`\*`).ReplaceAllString(line, `([^/]*)`) - - // Handle escaping the "?" char - line = strings.Replace(line, "?", `\?`, -1) - - line = strings.Replace(line, magicStar, "*", -1) - - // Temporary regex - var expr = "" - if strings.HasSuffix(line, "/") { - expr = line + "(|.*)$" - } else { - expr = line + "(|/.*)$" - } - if strings.HasPrefix(expr, "/") { - expr = "^(|/)" + expr[1:] - } else { - expr = "^(|.*/)" + expr - } - pattern, _ := regexp.Compile(expr) - - return pattern, negatePattern -} - -//////////////////////////////////////////////////////////// - -// ignorePattern encapsulates a pattern and if it is a negated pattern. -type ignorePattern struct { - pattern *regexp.Regexp - negate bool -} - -// GitIgnore wraps a list of ignore pattern. -type GitIgnore struct { - patterns []*ignorePattern -} - -// Accepts a variadic set of strings, and returns a GitIgnore object which -// converts and appends the lines in the input to regexp.Regexp patterns -// held within the GitIgnore objects "patterns" field -func CompileIgnoreLines(lines ...string) (*GitIgnore, error) { - gi := &GitIgnore{} - for _, line := range lines { - pattern, negatePattern := getPatternFromLine(line) - if pattern != nil { - ip := &ignorePattern{pattern, negatePattern} - gi.patterns = append(gi.patterns, ip) - } - } - return gi, nil -} - -// Accepts a ignore file as the input, parses the lines out of the file -// and invokes the CompileIgnoreLines method -func CompileIgnoreFile(fpath string) (*GitIgnore, error) { - buffer, error := ioutil.ReadFile(fpath) - if error == nil { - s := strings.Split(string(buffer), "\n") - return CompileIgnoreLines(s...) - } - - return CompileIgnoreLines("") -} - -// Accepts a ignore file as the input, parses the lines out of the file -// and invokes the CompileIgnoreLines method with additional lines -func CompileIgnoreFileAndLines(fpath string, lines ...string) (*GitIgnore, error) { - buffer, error := ioutil.ReadFile(fpath) - if error == nil { - s := strings.Split(string(buffer), "\n") - return CompileIgnoreLines(append(s, lines...)...) - } - return nil, error -} - -//////////////////////////////////////////////////////////// - -// MatchesPath returns true if the given GitIgnore structure would target -// a given path string `f`. -func (gi *GitIgnore) MatchesPath(f string) bool { - // Replace OS-specific path separator. - f = strings.Replace(f, string(os.PathSeparator), "/", -1) - - matchesPath := false - for _, ip := range gi.patterns { - if ip.pattern.MatchString(f) { - // If this is a regular target (not negated with a gitignore exclude "!" etc) - if !ip.negate { - matchesPath = true - } else if matchesPath { - // Negated pattern, and matchesPath is already set - matchesPath = false - } - } - } - return matchesPath -} - -//////////////////////////////////////////////////////////// diff --git a/gateway/main.go b/gateway/main.go deleted file mode 100644 index 12ae088..0000000 --- a/gateway/main.go +++ /dev/null @@ -1,193 +0,0 @@ -package gateway - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - errors2 "github.com/railwayapp/cli/errors" - "github.com/railwayapp/cli/ui" - "io" - "net/http" - "os" - "strings" - "time" - - "github.com/pkg/errors" - - configs "github.com/railwayapp/cli/configs" - "github.com/railwayapp/cli/constants" -) - -const ( - CLI_SOURCE_HEADER = "cli" -) - -type Gateway struct { - cfg *configs.Configs - httpClient *http.Client -} - -func GetHost() string { - baseURL := "https://backboard.railway.app" - if configs.IsDevMode() { - baseURL = "https://backboard.railway-develop.app" - } - if configs.IsStagingMode() { - baseURL = "https://backboard.railway-staging.app" - } - return baseURL -} - -type AttachCommonHeadersTransport struct{} - -func (t *AttachCommonHeadersTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Add("x-source", CLI_SOURCE_HEADER) - - version := constants.Version - if constants.IsDevVersion() { - version = "dev" - } - req.Header.Set("X-Railway-Version", version) - return http.DefaultTransport.RoundTrip(req) -} - -func New() *Gateway { - httpClient := &http.Client{ - Timeout: time.Second * 30, - Transport: &AttachCommonHeadersTransport{}, - } - - return &Gateway{ - cfg: configs.New(), - httpClient: httpClient, - } -} - -type GQLRequest struct { - q string - vars map[string]interface{} - header http.Header - httpClient *http.Client -} - -type GQLError struct { - Message string `json:"message"` -} - -func (e GQLError) Error() string { - return e.Message -} - -type GQLResponse struct { - Errors []GQLError `json:"errors"` - Data interface{} `json:"data"` -} - -func (g *Gateway) authorize(header http.Header) error { - if g.cfg.RailwayProductionToken != "" { - header.Add("project-access-token", g.cfg.RailwayProductionToken) - } else { - user, err := g.cfg.GetUserConfigs() - if err != nil { - return err - } - header.Add("authorization", fmt.Sprintf("Bearer %s", user.Token)) - } - - return nil -} - -func (g *Gateway) NewRequestWithoutAuth(query string) *GQLRequest { - gqlReq := &GQLRequest{ - q: query, - header: http.Header{}, - httpClient: g.httpClient, - vars: make(map[string]interface{}), - } - - return gqlReq -} - -func (g *Gateway) NewRequestWithAuth(query string) (*GQLRequest, error) { - gqlReq := g.NewRequestWithoutAuth(query) - - err := g.authorize(gqlReq.header) - if err != nil { - return gqlReq, err - } - - return gqlReq, nil -} - -func (r *GQLRequest) Run(ctx context.Context, resp interface{}) error { - var requestBody bytes.Buffer - requestBodyObj := struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - }{ - Query: r.q, - Variables: r.vars, - } - if err := json.NewEncoder(&requestBody).Encode(requestBodyObj); err != nil { - return errors.Wrap(err, "encode body") - } - - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/graphql", GetHost()), &requestBody) - if err != nil { - return err - } - - req = req.WithContext(ctx) - req.Header = r.header - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json; charset=utf-8") - res, err := r.httpClient.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - var buf bytes.Buffer - if _, err := io.Copy(&buf, res.Body); err != nil { - return err - } - - // TODO: Handle auth errors and other things in a special way - if res.StatusCode < 200 || res.StatusCode >= 300 { - return fmt.Errorf("Response not successful status=%d", res.StatusCode) - } - - gr := &GQLResponse{ - Data: resp, - } - if err := json.NewDecoder(&buf).Decode(&gr); err != nil { - return errors.Wrap(err, "decoding response") - } - if len(gr.Errors) > 0 { - messages := make([]string, len(gr.Errors)) - for i, err := range gr.Errors { - messages[i] = err.Error() - } - - errText := gr.Errors[0].Message - if len(gr.Errors) > 1 { - errText = fmt.Sprintf("%d Errors: %s", len(gr.Errors), strings.Join(messages, ", ")) - } - - // If any GQL responses return fail because unauthenticated, print an error telling the - // user to log in and exit immediately - if strings.Contains(errText, "Not Authorized") { - println(ui.AlertDanger(errors2.UserNotAuthorized.Error())) - os.Exit(1) - } - - return errors.New(errText) - } - - return nil -} - -func (r *GQLRequest) Var(name string, value interface{}) { - r.vars[name] = value -} diff --git a/gateway/panic.go b/gateway/panic.go deleted file mode 100644 index b82d42d..0000000 --- a/gateway/panic.go +++ /dev/null @@ -1,33 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -func (g *Gateway) SendPanic(ctx context.Context, req *entity.PanicRequest) (bool, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($command: String!, $error: String!, $stacktrace: String!, $projectId: String, $environmentId: String) { - sendTelemetry(command: $command, error: $error, stacktrace: $stacktrace, projectId: $projectId, environmentId: $environmentId) - } - `) - if err != nil { - return false, err - } - - gqlReq.Var("command", req.Command) - gqlReq.Var("error", req.PanicError) - gqlReq.Var("stacktrace", req.Stacktrace) - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("environmentId", req.EnvironmentID) - - var resp struct { - Status bool `json:"sendTelemetry"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return false, errors.TelemetryFailed - } - return resp.Status, nil -} diff --git a/gateway/plugin.go b/gateway/plugin.go deleted file mode 100644 index fef91a9..0000000 --- a/gateway/plugin.go +++ /dev/null @@ -1,54 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -func (g *Gateway) GetAvailablePlugins(ctx context.Context, projectId string) ([]string, error) { - gqlReq, err := g.NewRequestWithAuth(` - query ($projectId: ID!) { - availablePluginsForProject(projectId: $projectId) - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", projectId) - - var resp struct { - Plugins []string `json:"availablePluginsForProject"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.PluginGetFailed - } - return resp.Plugins, nil -} - -func (g *Gateway) CreatePlugin(ctx context.Context, req *entity.CreatePluginRequest) (*entity.Plugin, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($projectId: String!, $name: String!) { - createPlugin(projectId: $projectId, name: $name) { - id, - name - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", req.ProjectID) - gqlReq.Var("name", req.Plugin) - - var resp struct { - Plugin *entity.Plugin `json:"createPlugin"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.PluginCreateFailed - } - return resp.Plugin, nil -} diff --git a/gateway/project.go b/gateway/project.go deleted file mode 100644 index 426847b..0000000 --- a/gateway/project.go +++ /dev/null @@ -1,312 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - - "github.com/pkg/browser" - configs "github.com/railwayapp/cli/configs" - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -// GetProjectToken looks up a project and environment by the RAILWAY_TOKEN -func (g *Gateway) GetProjectToken(ctx context.Context) (*entity.ProjectToken, error) { - if g.cfg.RailwayProductionToken == "" { - return nil, errors.ProjectTokenNotFound - } - - gqlReq, err := g.NewRequestWithAuth(` - query { - projectToken { - projectId - environmentId - } - } - `) - if err != nil { - return nil, err - } - - var resp struct { - ProjectToken *entity.ProjectToken `json:"projectToken"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProjectTokenNotFound - } - return resp.ProjectToken, nil -} - -// GetProject returns the project associated with the projectId, as well as -// it's environments, plugins, etc -func (g *Gateway) GetProject(ctx context.Context, projectId string) (*entity.Project, error) { - gqlReq, err := g.NewRequestWithAuth(` - query ($projectId: ID!) { - projectById(projectId: $projectId) { - id, - name, - plugins { - id, - name, - }, - environments { - id, - name - }, - services { - id, - name - }, - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", projectId) - - var resp struct { - Project *entity.Project `json:"projectById"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProjectConfigNotFound - } - return resp.Project, nil -} - -func (g *Gateway) GetProjectByName(ctx context.Context, projectName string) (*entity.Project, error) { - gqlReq, err := g.NewRequestWithAuth(` - query ($projectName: String!) { - me { - projects(where: { name: { equals: $projectName } }) { - id, - name, - plugins { - id, - name, - }, - environments { - id, - name - }, - } - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectName", projectName) - - var resp struct { - Me struct { - Projects []*entity.Project `json:"projects"` - } `json:"me"` - } - - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProjectConfigNotFound - } - - projects := resp.Me.Projects - if len(projects) == 0 { - return nil, errors.ProjectConfigNotFound - } - - return projects[0], nil -} - -func (g *Gateway) CreateProject(ctx context.Context, req *entity.CreateProjectRequest) (*entity.Project, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($name: String) { - createProject(name: $name) { - id, - name - environments { - id - name - } - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("name", req.Name) - - var resp struct { - Project *entity.Project `json:"createProject"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProjectCreateFailed - } - return resp.Project, nil -} - -func (g *Gateway) CreateProjectFromTemplate(ctx context.Context, req *entity.CreateProjectFromTemplateRequest) (*entity.CreateProjectFromTemplateResult, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($name: String!, $owner: String!, $template: String!, $isPrivate: Boolean, $plugins: [String!], $variables: Json) { - createProjectFromTemplate(name: $name, owner: $owner, template: $template, isPrivate: $isPrivate, plugins: $plugins, variables: $variables) { - projectId - workflowId - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("name", req.Name) - gqlReq.Var("owner", req.Owner) - gqlReq.Var("template", req.Template) - gqlReq.Var("isPrivate", req.IsPrivate) - gqlReq.Var("plugins", req.Plugins) - gqlReq.Var("variables", req.Variables) - - var resp struct { - Result *entity.CreateProjectFromTemplateResult `json:"createProjectFromTemplate"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProjectCreateFromTemplateFailed - } - return resp.Result, nil -} - -func (g *Gateway) UpdateProject(ctx context.Context, req *entity.UpdateProjectRequest) (*entity.Project, error) { - gqlReq, err := g.NewRequestWithAuth(` - mutation($projectId: ID!) { - updateProject(projectId: $projectId) { - id, - name - } - } - `) - if err != nil { - return nil, err - } - - gqlReq.Var("projectId", req.Id) - var resp struct { - Project *entity.Project `json:"createProject"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, err - } - return resp.Project, nil -} - -func (g *Gateway) DeleteProject(ctx context.Context, projectId string) error { - gqlReq, err := g.NewRequestWithAuth(` - mutation($projectId: String!) { - deleteProject(projectId: $projectId) - } - `) - if err != nil { - return err - } - - gqlReq.Var("projectId", projectId) - var resp struct { - Deleted bool `json:"deleteProject"` - } - return gqlReq.Run(ctx, &resp) -} - -// GetProjects returns all projects associated with the user, as well as -// their environments associated with those projects, error otherwise -// Performs a dual join -func (g *Gateway) GetProjects(ctx context.Context) ([]*entity.Project, error) { - projectFrag := ` - id, - updatedAt, - name, - plugins { - id, - name, - }, - environments { - id, - name - }, - ` - - gqlReq, err := g.NewRequestWithAuth(fmt.Sprintf(` - query { - me { - name - projects { - %s - } - teams { - name - projects { - %s - } - } - } - } - `, projectFrag, projectFrag)) - if err != nil { - return nil, err - } - - var resp struct { - Me struct { - Name *string `json:"name"` - Projects []*entity.Project `json:"projects"` - Teams []*struct { - Name string `json:"name"` - Projects []*entity.Project `json:"projects"` - } `json:"teams"` - } `json:"me"` - } - - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProblemFetchingProjects - } - - projects := resp.Me.Projects - - for _, project := range resp.Me.Projects { - name := "Me" - if resp.Me.Name != nil { - name = *resp.Me.Name - } - project.Team = &name - } - for _, team := range resp.Me.Teams { - for _, project := range team.Projects { - project.Team = &team.Name - } - projects = append(projects, team.Projects...) - } - - return projects, nil -} - -func (g *Gateway) OpenProjectInBrowser(projectID string, environmentID string) error { - return browser.OpenURL(fmt.Sprintf("%s/project/%s?environmentId=%s", configs.GetRailwayURL(), projectID, environmentID)) -} - -func (g *Gateway) OpenProjectPathInBrowser(projectID string, environmentID string, path string) error { - return browser.OpenURL(fmt.Sprintf("%s/project/%s/%s?environmentId=%s", configs.GetRailwayURL(), projectID, path, environmentID)) -} - -func (g *Gateway) OpenProjectDeploymentsInBrowser(projectID string) error { - return browser.OpenURL(g.GetProjectDeploymentsURL(projectID)) -} - -func (g *Gateway) GetProjectDeploymentsURL(projectID string) string { - return fmt.Sprintf("%s/project/%s/deployments?open=true", configs.GetRailwayURL(), projectID) -} - -func (g *Gateway) GetServiceDeploymentsURL(projectID string, serviceID string, deploymentID string) string { - return fmt.Sprintf("%s/project/%s/service/%s?id=%s", configs.GetRailwayURL(), projectID, serviceID, deploymentID) -} - -func (g *Gateway) OpenStaticUrlInBrowser(staticUrl string) error { - return browser.OpenURL(fmt.Sprintf("https://%s", staticUrl)) -} diff --git a/gateway/scope.go b/gateway/scope.go deleted file mode 100644 index a30e13c..0000000 --- a/gateway/scope.go +++ /dev/null @@ -1,27 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/errors" -) - -// GetWritableGithubScopes returns scopes associated with Railway user -func (g *Gateway) GetWritableGithubScopes(ctx context.Context) ([]string, error) { - gqlReq, err := g.NewRequestWithAuth(` - query { - getWritableGithubScopes - } - `) - if err != nil { - return nil, err - } - - var resp struct { - Scopes []string `json:"getWritableGithubScopes"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, errors.ProblemFetchingWritableGithubScopes - } - return resp.Scopes, nil -} diff --git a/gateway/starter.go b/gateway/starter.go deleted file mode 100644 index a77f148..0000000 --- a/gateway/starter.go +++ /dev/null @@ -1,30 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (g *Gateway) GetStarters(ctx context.Context) ([]*entity.Starter, error) { - gqlReq := g.NewRequestWithoutAuth(` - query { - getAllStarters { - title - url - source - } - } - `) - - var resp struct { - Starters []*entity.Starter `json:"getAllStarters"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, err - } - - starters := resp.Starters - - return starters, nil -} diff --git a/gateway/up.go b/gateway/up.go deleted file mode 100644 index bdd88cc..0000000 --- a/gateway/up.go +++ /dev/null @@ -1,64 +0,0 @@ -package gateway - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - - "github.com/railwayapp/cli/entity" -) - -func constructReq(ctx context.Context, req *entity.UpRequest) (*http.Request, error) { - url := fmt.Sprintf("%s/project/%s/environment/%s/up?serviceId=%s", GetHost(), req.ProjectID, req.EnvironmentID, req.ServiceID) - httpReq, err := http.NewRequestWithContext(ctx, "POST", url, &req.Data) - if err != nil { - return nil, err - } - httpReq.Header.Set("Content-Type", "multipart/form-data") - - return httpReq, nil -} - -func (g *Gateway) Up(ctx context.Context, req *entity.UpRequest) (*entity.UpResponse, error) { - httpReq, err := constructReq(ctx, req) - if err != nil { - return nil, err - } - err = g.authorize(httpReq.Header) - if err != nil { - return nil, err - } - - // The `up` command uses a custom HTTP Client so there is no timeout on the requests - client := &http.Client{ - Transport: &AttachCommonHeadersTransport{}, - } - - resp, err := client.Do(httpReq) - if err != nil { - return nil, err - } - - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode < 200 || resp.StatusCode >= 400 { - var res entity.UpErrorResponse - // Try decoding up's error response and fallback to sending body as text if decoding fails - if err := json.Unmarshal(bodyBytes, &res); err != nil { - return nil, errors.New(string(bodyBytes)) - } - return nil, errors.New(res.Message) - } - - var res entity.UpResponse - if err := json.Unmarshal(bodyBytes, &res); err != nil { - return nil, err - } - return &res, nil -} diff --git a/gateway/user.go b/gateway/user.go deleted file mode 100644 index 817e6c1..0000000 --- a/gateway/user.go +++ /dev/null @@ -1,77 +0,0 @@ -package gateway - -import ( - "context" - - "github.com/railwayapp/cli/entity" -) - -func (g *Gateway) GetUser(ctx context.Context) (*entity.User, error) { - gqlReq, err := g.NewRequestWithAuth(` - query { - me { - id, - email, - name, - has2FA - } - } - `) - if err != nil { - return nil, err - } - - var resp struct { - User *entity.User `json:"me"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return nil, err - } - return resp.User, nil -} - -func (g *Gateway) CreateLoginSession(ctx context.Context) (string, error) { - gqlReq := g.NewRequestWithoutAuth(`mutation { createLoginSession } `) - - var resp struct { - Code string `json:"createLoginSession"` - } - - if err := gqlReq.Run(ctx, &resp); err != nil { - return "", err - } - - return resp.Code, nil -} - -func (g *Gateway) ConsumeLoginSession(ctx context.Context, code string) (string, error) { - gqlReq := g.NewRequestWithoutAuth(` - mutation($code: String!) { - consumeLoginSession(code: $code) - } - `) - gqlReq.Var("code", code) - - var resp struct { - Token string `json:"consumeLoginSession"` - } - - if err := gqlReq.Run(ctx, &resp); err != nil { - return "", err - } - - return resp.Token, nil -} - -func (g *Gateway) Logout(ctx context.Context) error { - gqlReq, err := g.NewRequestWithAuth(`mutation { logout }`) - if err != nil { - return err - } - - if err := gqlReq.Run(ctx, nil); err != nil { - return err - } - - return nil -} diff --git a/gateway/workflow.go b/gateway/workflow.go deleted file mode 100644 index 656dabb..0000000 --- a/gateway/workflow.go +++ /dev/null @@ -1,31 +0,0 @@ -package gateway - -import ( - context "context" - - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/errors" -) - -func (g *Gateway) GetWorkflowStatus(ctx context.Context, workflowID string) (entity.WorkflowStatus, error) { - gqlReq, err := g.NewRequestWithAuth(` - query($workflowId: String!) { - getWorkflowStatus(workflowId: $workflowId) { - status - } - } - `) - if err != nil { - return "", err - } - - gqlReq.Var("workflowId", workflowID) - - var resp struct { - WorkflowStatus *entity.WorkflowStatusResponse `json:"getWorkflowStatus"` - } - if err := gqlReq.Run(ctx, &resp); err != nil { - return "", errors.ProjectCreateFailed - } - return resp.WorkflowStatus.Status, nil -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 059678a..0000000 --- a/go.mod +++ /dev/null @@ -1,34 +0,0 @@ -module github.com/railwayapp/cli - -go 1.14 - -require ( - github.com/briandowns/spinner v1.11.1 - github.com/fatih/color v1.9.0 // indirect - github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/google/go-github v17.0.0+incompatible - github.com/google/go-querystring v1.0.0 // indirect - github.com/google/uuid v1.2.0 - github.com/joho/godotenv v1.4.0 - github.com/logrusorgru/aurora v2.0.3+incompatible - github.com/lunixbochs/vtclean v1.0.0 // indirect - github.com/magiconair/properties v1.8.3 // indirect - github.com/manifoldco/promptui v0.7.0 - github.com/mattn/go-colorable v0.1.7 - github.com/mattn/go-isatty v0.0.12 - github.com/mitchellh/mapstructure v1.3.3 // indirect - github.com/pelletier/go-toml v1.8.1 // indirect - github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 - github.com/pkg/errors v0.8.1 - github.com/spf13/afero v1.4.0 // indirect - github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v1.0.0 - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.6.1 - golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/ini.v1 v1.61.0 // indirect - gopkg.in/yaml.v2 v2.3.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 419ba85..0000000 --- a/go.sum +++ /dev/null @@ -1,384 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0= -github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.3 h1:kJSsc6EXkBLgr3SphHk9w5mtjn0bjlR4JYEXKrJ45rQ= -github.com/magiconair/properties v1.8.3/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= -github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= -github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8= -github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10= -gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/install.sh b/install.sh index 5a89d81..9627741 100755 --- a/install.sh +++ b/install.sh @@ -1,201 +1,510 @@ -#!/bin/sh -set -e +#!/usr/bin/env bash -# -# Railway CLI -# -# See https://railway.docs/cli for installation instructions -# -# This script is meant for quick installs via sh -# sh -c "$(curl -sSL https://raw.githubusercontent.com/railwayapp/cli/master/install.sh)" -# +# Adapted from https://github.com/starship/starship/blob/master/install/install.sh -INSTALL_DIR=${INSTALL_DIR:-"/usr/local/bin"} -BINARY_NAME=${BINARY_NAME:-"railway"} +help_text="Options -REPO_NAME="railwayapp/cli" -ISSUE_URL="https://github.com/railwayapp/cli/issues/new" + -V, --verbose + Enable verbose output for the installer -# Usage -# get_latest_release "railwayapp/cli" -get_latest_release() { - curl --silent "https://api.github.com/repos/$1/releases/latest" | # Get latest release from GitHub api - grep '"tag_name":' | # Get tag line - sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value + -f, -y, --force, --yes + Skip the confirmation prompt during installation + + -p, --platform + Override the platform identified by the installer + + -b, --bin-dir + Override the bin installation directory + + -a, --arch + Override the architecture identified by the installer + + -B, --base-url + Override the base URL used for downloading releases + + -r, --remove + Uninstall railway + + -h, --help + Get some help + +" + +set -eu +printf '\n' + +BOLD="$(tput bold 2>/dev/null || printf '')" +GREY="$(tput setaf 0 2>/dev/null || printf '')" +UNDERLINE="$(tput smul 2>/dev/null || printf '')" +RED="$(tput setaf 1 2>/dev/null || printf '')" +GREEN="$(tput setaf 2 2>/dev/null || printf '')" +YELLOW="$(tput setaf 3 2>/dev/null || printf '')" +BLUE="$(tput setaf 4 2>/dev/null || printf '')" +MAGENTA="$(tput setaf 5 2>/dev/null || printf '')" +NO_COLOR="$(tput sgr0 2>/dev/null || printf '')" + +SUPPORTED_TARGETS="x86_64-unknown-linux-gnu x86_64-unknown-linux-musl \ + i686-unknown-linux-musl aarch64-unknown-linux-musl \ + arm-unknown-linux-musleabihf x86_64-apple-darwin \ + aarch64-apple-darwin x86_64-pc-windows-msvc \ + i686-pc-windows-msvc aarch64-pc-windows-msvc \ + x86_64-unknown-freebsd" + +info() { + printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*" } -get_asset_name() { - echo "railway_$1_$2_$3.tar.gz" -} - -get_download_url() { - local asset_name=$(get_asset_name $1 $2 $3) - echo "https://github.com/railwayapp/cli/releases/download/v$1/${asset_name}" -} - -get_checksum_url() { - echo "https://github.com/railwayapp/cli/releases/download/v$1/railway_$1_checksums.txt" -} - -command_exists() { - command -v "$@" >/dev/null 2>&1 -} - -fmt_error() { - echo ${RED}"Error: $@"${RESET} >&2 -} - -fmt_warning() { - echo ${YELLOW}"Warning: $@"${RESET} >&2 -} - -fmt_underline() { - echo "$(printf '\033[4m')$@$(printf '\033[24m')" -} - -fmt_code() { - echo "\`$(printf '\033[38;5;247m')$@${RESET}\`" -} - -setup_color() { - # Only use colors if connected to a terminal - if [ -t 1 ]; then - RED=$(printf '\033[31m') - GREEN=$(printf '\033[32m') - YELLOW=$(printf '\033[33m') - BLUE=$(printf '\033[34m') - MAGENTA=$(printf '\033[35m') - BOLD=$(printf '\033[1m') - RESET=$(printf '\033[m') - else - RED="" - GREEN="" - YELLOW="" - BLUE="" - MAGENTA="" - BOLD="" - RESET="" +debug() { + if [[ -n "${VERBOSE}" ]]; then + printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*" fi } -get_os() { - case "$(uname -s)" in - *linux* ) echo "linux" ;; - *Linux* ) echo "linux" ;; - *darwin* ) echo "darwin" ;; - *Darwin* ) echo "darwin" ;; - esac +warn() { + printf '%s\n' "${YELLOW}! $*${NO_COLOR}" } -get_machine() { - case "$(uname -m)" in - "x86_64"|"amd64"|"x64") - echo "amd64" ;; - "i386"|"i86pc"|"x86"|"i686") - echo "i386" ;; - "arm64"|"armv6l"|"aarch64") - echo "arm64" - esac +error() { + printf '%s\n' "${RED}x $*${NO_COLOR}" >&2 } -get_tmp_dir() { - echo $(mktemp -d) +completed() { + printf '%s\n' "${GREEN}✓${NO_COLOR} $*" } -do_checksum() { - checksum_url=$(get_checksum_url $version) - expected_checksum=$(curl -sL $checksum_url | grep $asset_name | awk '{print $1}') +has() { + command -v "$1" 1>/dev/null 2>&1 +} - if command_exists sha256sum; then - checksum=$(sha256sum $asset_name | awk '{print $1}') - elif command_exists shasum; then - checksum=$(shasum -a 256 $asset_name | awk '{print $1}') +# Gets path to a temporary file, even if +get_tmpfile() { + local suffix + suffix="$1" + if has mktemp; then + printf "%s%s.%s.%s" "$(mktemp)" "-rlwy" "${RANDOM}" "${suffix}" else - fmt_warning "Could not find a checksum program. Install shasum or sha256sum to validate checksum." + # No really good options here--let's pick a default + hope + printf "/tmp/rlwy.%s" "${suffix}" + fi +} + +# Test if a location is writeable by trying to write to it. Windows does not let +# you test writeability other than by writing: https://stackoverflow.com/q/1999988 +test_writeable() { + local path + path="${1:-}/test.txt" + if touch "${path}" 2>/dev/null; then + rm "${path}" return 0 - fi - - if [ "$checksum" != "$expected_checksum" ]; then - fmt_error "Checksums do not match" - exit 1 - fi -} - -do_install_binary() { - asset_name=$(get_asset_name $version $os $machine) - download_url=$(get_download_url $version $os $machine) - - command_exists curl || { - fmt_error "curl is not installed" - exit 1 - } - - command_exists tar || { - fmt_error "tar is not installed" - exit 1 - } - - local tmp_dir=$(get_tmp_dir) - - # Download tar.gz to tmp directory - echo "Downloading $download_url" - (cd $tmp_dir && curl -sL -O "$download_url") - - (cd $tmp_dir && do_checksum) - - # Extract download - (cd $tmp_dir && tar -xzf "$asset_name") - - # Install binary - mv "$tmp_dir/$BINARY_NAME" $INSTALL_DIR - echo "Installed railway to $INSTALL_DIR" - - # Cleanup - rm -rf $tmp_dir -} - -install_termux() { - echo "Installing, this may take a few minutes..." - pkg upgrade && pkg install golang git -y && git clone https://github.com/railwayapp/cli.git && cd cli/ && go build -o $PREFIX/bin/railway -} - -main() { - setup_color - - latest_tag=$(get_latest_release $REPO_NAME) - latest_version=$(echo $latest_tag | sed 's/v//') - version=${VERSION:-$latest_version} - - os=$(get_os) - if test -z "$os"; then - fmt_error "$(uname -s) os type is not supported" - echo "Please create an issue so we can add support. $ISSUE_URL" - exit 1 - fi - - machine=$(get_machine) - if test -z "$machine"; then - fmt_error "$(uname -m) machine type is not supported" - echo "Please create an issue so we can add support. $ISSUE_URL" - exit 1 - fi - if [ ${TERMUX_VERSION} ] ; then - install_termux else - do_install_binary + return 1 fi +} + +download() { + file="$1" + url="$2" + touch "$file" + + if has curl; then + cmd="curl --fail --silent --location --output $file $url" + elif has wget; then + cmd="wget --quiet --output-document=$file $url" + elif has fetch; then + cmd="fetch --quiet --output=$file $url" + else + error "No HTTP download program (curl, wget, fetch) found, exiting…" + return 1 + fi + + $cmd && return 0 || rc=$? + + error "Command failed (exit code $rc): ${BLUE}${cmd}${NO_COLOR}" + printf "\n" >&2 + info "This is likely due to rlwy not yet supporting your configuration." + info "If you would like to see a build for your configuration," + info "please create an issue requesting a build for ${MAGENTA}${TARGET}${NO_COLOR}:" + info "${BOLD}${UNDERLINE}https://github.com/railwayapp/cliv3/issues/new/${NO_COLOR}" + return $rc +} + +unpack() { + local archive=$1 + local bin_dir=$2 + local sudo=${3-} + + case "$archive" in + *.tar.gz) + flags=$(test -n) + ${sudo} tar "${flags}" -xzf "${archive}" -C "${bin_dir}" + return 0 + ;; + *.zip) + flags=$(test -z) + UNZIP="${flags}" ${sudo} unzip "${archive}" -d "${bin_dir}" + return 0 + ;; + esac + + error "Unknown package extension." + printf "\n" + info "This almost certainly results from a bug in this script--please file a" + info "bug report at https://github.com/railwayapp/cliv3/issues" + return 1 +} + +elevate_priv() { + if ! has sudo; then + error 'Could not find the command "sudo", needed to get permissions for install.' + info "If you are on Windows, please run your shell as an administrator, then" + info "rerun this script. Otherwise, please run this script as root, or install" + info "sudo." + exit 1 + fi + if ! sudo -v; then + error "Superuser not granted, aborting installation" + exit 1 + fi +} + +install() { + local msg + local sudo + local archive + local ext="$1" + + if test_writeable "${BIN_DIR}"; then + sudo="" + msg="Installing rlwy, please wait…" + else + warn "Escalated permissions are required to install to ${BIN_DIR}" + elevate_priv + sudo="sudo" + msg="Installing rlwy as root, please wait…" + fi + info "$msg" + + archive=$(get_tmpfile "$ext") + + # download to the temp file + download "${archive}" "${URL}" + + # unpack the temp file to the bin dir, using sudo if required + unpack "${archive}" "${BIN_DIR}" "${sudo}" + + # remove tempfile + + # rm "${archive}" +} + +# Currently supporting: +# - win (Git Bash) +# - darwin +# - linux +# - linux_musl (Alpine) +# - freebsd +detect_platform() { + local platform + platform="$(uname -s | tr '[:upper:]' '[:lower:]')" + + case "${platform}" in + msys_nt*) platform="pc-windows-msvc" ;; + cygwin_nt*) platform="pc-windows-msvc";; + # mingw is Git-Bash + mingw*) platform="pc-windows-msvc" ;; + # use the statically compiled musl bins on linux to avoid linking issues. + linux) platform="unknown-linux-musl" ;; + darwin) platform="apple-darwin" ;; + freebsd) platform="unknown-freebsd" ;; + esac + + printf '%s' "${platform}" +} + +# Currently supporting: +# - x86_64 +# - i386 +detect_arch() { + local arch + arch="$(uname -m | tr '[:upper:]' '[:lower:]')" + + case "${arch}" in + amd64) arch="x86_64" ;; + armv*) arch="arm" ;; + arm64) arch="aarch64" ;; + esac + + # `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check + if [ "${arch}" = "x86_64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then + arch=i686 + elif [ "${arch}" = "aarch64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then + arch=arm + fi + + printf '%s' "${arch}" +} + +detect_target() { + local arch="$1" + local platform="$2" + local target="$arch-$platform" + + if [ "${target}" = "arm-unknown-linux-musl" ]; then + target="${target}eabihf" + fi + + printf '%s' "${target}" +} + + +confirm() { + if [ -t 0 ]; then + if [ -z "${FORCE-}" ]; then + printf "%s " "${MAGENTA}?${NO_COLOR} $* ${BOLD}[y/N]${NO_COLOR}" + set +e + read -r yn &2 + info "If you would like to see a build for your configuration," + info "please create an issue requesting a build for ${MAGENTA}${target}${NO_COLOR}:" + info "${BOLD}${UNDERLINE}https://github.com/railwayapp/cliv3/issues/new/${NO_COLOR}" + printf "\n" + exit 1 + fi +} +UNINSTALL=0 +HELP=0 +CARGOTOML="$(curl -fsSL https://raw.githubusercontent.com/railwayapp/cliv3/master/Cargo.toml)" +ALL_VERSIONS="$(sed -n 's/.*version = "\([^"]*\)".*/\1/p' <<< "$CARGOTOML")" +IFS=$'\n' read -r -a VERSION <<< "$ALL_VERSIONS" +DEFAULT_VERSION="$VERSION" + +# defaults +if [ -z "${NIXPACKS_VERSION-}" ]; then + NIXPACKS_VERSION="$DEFAULT_VERSION" +fi + +if [ -z "${NIXPACKS_PLATFORM-}" ]; then + PLATFORM="$(detect_platform)" +fi + +if [ -z "${NIXPACKS_BIN_DIR-}" ]; then + BIN_DIR=/usr/local/bin +fi + +if [ -z "${NIXPACKS_ARCH-}" ]; then + ARCH="$(detect_arch)" +fi + +if [ -z "${NIXPACKS_BASE_URL-}" ]; then + BASE_URL="https://github.com/railwayapp/cliv3/releases" +fi + +# parse argv variables +while [ "$#" -gt 0 ]; do + case "$1" in + -p | --platform) + PLATFORM="$2" + shift 2 + ;; + -b | --bin-dir) + BIN_DIR="$2" + shift 2 + ;; + -a | --arch) + ARCH="$2" + shift 2 + ;; + -B | --base-url) + BASE_URL="$2" + shift 2 + ;; + + -V | --verbose) + VERBOSE=1 + shift 1 + ;; + -f | -y | --force | --yes) + FORCE=1 + shift 1 + ;; + -r | --remove | --uninstall) + UNINSTALL=1 + shift 1 + ;; + -h | --help) + HELP=1 + shift 1 + ;; + -p=* | --platform=*) + PLATFORM="${1#*=}" + shift 1 + ;; + -b=* | --bin-dir=*) + BIN_DIR="${1#*=}" + shift 1 + ;; + -a=* | --arch=*) + ARCH="${1#*=}" + shift 1 + ;; + -B=* | --base-url=*) + BASE_URL="${1#*=}" + shift 1 + ;; + -V=* | --verbose=*) + VERBOSE="${1#*=}" + shift 1 + ;; + -f=* | -y=* | --force=* | --yes=*) + FORCE="${1#*=}" + shift 1 + ;; + + *) + error "Unknown option: $1" + exit 1 + ;; + esac +done + +# non-empty VERBOSE enables verbose untarring +if [ -n "${VERBOSE-}" ]; then + VERBOSE=v +else + VERBOSE= +fi + +if [ $UNINSTALL == 1 ]; then + confirm "Are you sure you want to uninstall rlwy?" + + msg="" + sudo="" + + info "REMOVING rlwy" + + if test_writeable "$(dirname "$(which rlwy)")"; then + sudo="" + msg="Removing rlwy, please wait…" + else + warn "Escalated permissions are required to install to ${BIN_DIR}" + elevate_priv + sudo="sudo" + msg="Removing rlwy as root, please wait…" + fi + + info "$msg" + ${sudo} rm "$(which rlwy)" + ${sudo} rm /tmp/rlwy + + info "Removed rlwy" + exit 0 - printf "$MAGENTA" + fi +if [ $HELP == 1 ]; then + echo "${help_text}" + exit 0 +fi +TARGET="$(detect_target "${ARCH}" "${PLATFORM}")" + +is_build_available "${ARCH}" "${PLATFORM}" "${TARGET}" + + +print_configuration () { + if [[ -n "${VERBOSE}" ]]; then + printf " %s\n" "${UNDERLINE}Configuration${NO_COLOR}" + debug "${BOLD}Bin directory${NO_COLOR}: ${GREEN}${BIN_DIR}${NO_COLOR}" + debug "${BOLD}Platform${NO_COLOR}: ${GREEN}${PLATFORM}${NO_COLOR}" + debug "${BOLD}Arch${NO_COLOR}: ${GREEN}${ARCH}${NO_COLOR}" + debug "${BOLD}Version${NO_COLOR}: ${GREEN}${NIXPACKS_VERSION}${NO_COLOR}" + printf '\n' + fi +} + +print_configuration + + +EXT=tar.gz +if [ "${PLATFORM}" = "pc-windows-msvc" ]; then + EXT=zip +fi + +URL="${BASE_URL}/download/v${NIXPACKS_VERSION}/rlwy-v${NIXPACKS_VERSION}-${TARGET}.${EXT}" +debug "Tarball URL: ${UNDERLINE}${BLUE}${URL}${NO_COLOR}" +confirm "Install rlwy ${GREEN}${NIXPACKS_VERSION}${NO_COLOR} to ${BOLD}${GREEN}${BIN_DIR}${NO_COLOR}?" +check_bin_dir "${BIN_DIR}" + +install "${EXT}" + +printf "$MAGENTA" cat <<'EOF' . /^\ . /\ "V" /__\ I O o - //..\\ I . Poof! + //..\\ I . Poof! \].`[/ I /l\/j\ (] . O /. ~~ ,\/I . Railway is now installed - \\L__j^\/I o Run `railway help` for commands + \\L__j^\/I o Run `rlwy help` for commands \/--v} I o . | | I _________ | | I c(` ')o @@ -203,8 +512,4 @@ main() { _/j L l\_! _//^---^\\_ EOF - printf "$RESET" - -} - -main +printf "$NO_COLOR" \ No newline at end of file diff --git a/lib/gql/gql.go b/lib/gql/gql.go deleted file mode 100644 index 01fec59..0000000 --- a/lib/gql/gql.go +++ /dev/null @@ -1,41 +0,0 @@ -package gql - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" -) - -func AsGQL(ctx context.Context, req interface{}) (*string, error) { - mp := make(map[string]interface{}) - bytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - err = json.Unmarshal(bytes, &mp) - if err != nil { - return nil, err - } - fields := []string{} - for k, i := range mp { - // GQL Selection - switch i.(type) { - case bool: - // GQL Selection - fields = append(fields, k) - case map[string]interface{}: - // Nested GQL/Struct - nested, err := AsGQL(ctx, i) - if err != nil { - return nil, err - } - fields = append(fields, fmt.Sprintf("%s {\n%s\n}", k, *nested)) - default: - return nil, errors.New("Unsupported Type! Cannot generate GQL") - } - } - q := strings.Join(fields, "\n") - return &q, nil -} diff --git a/main.go b/main.go deleted file mode 100644 index 7c617b2..0000000 --- a/main.go +++ /dev/null @@ -1,361 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "runtime" - "runtime/debug" - "strings" - - "github.com/railwayapp/cli/cmd" - "github.com/railwayapp/cli/constants" - "github.com/railwayapp/cli/entity" - "github.com/railwayapp/cli/ui" - "github.com/spf13/cobra" -) - -var rootCmd = &cobra.Command{ - Use: "railway", - SilenceUsage: true, - SilenceErrors: true, - Version: constants.Version, - Short: "🚅 Railway. Infrastructure, Instantly.", - Long: "Interact with 🚅 Railway via CLI \n\n Deploy infrastructure, instantly. Docs: https://docs.railway.app", -} - -func addRootCmd(cmd *cobra.Command) *cobra.Command { - rootCmd.AddCommand(cmd) - return cmd -} - -// contextualize converts a HandlerFunction to a cobra function -func contextualize(fn entity.HandlerFunction, panicFn entity.PanicFunction) entity.CobraFunction { - return func(cmd *cobra.Command, args []string) error { - ctx := context.Background() - - defer func() { - // Skip recover during development, so we can see the panic stack traces instead of going - // through the "send to Railway" flow and hiding the stack from the user - if constants.IsDevVersion() { - return - } - - if r := recover(); r != nil { - err := panicFn(ctx, fmt.Sprint(r), string(debug.Stack()), cmd.Name(), args) - if err != nil { - fmt.Println("Unable to relay panic to server. Are you connected to the internet?") - } - } - }() - - req := &entity.CommandRequest{ - Cmd: cmd, - Args: args, - } - err := fn(ctx, req) - if err != nil { - fmt.Println(ui.AlertDanger(err.Error())) - os.Exit(1) // Set non-success exit code on error - } - return nil - } -} - -func init() { - // Initializes all commands - handler := cmd.New() - - rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Print verbose output") - - loginCmd := addRootCmd(&cobra.Command{ - Use: "login", - Short: "Login to your Railway account", - RunE: contextualize(handler.Login, handler.Panic), - }) - loginCmd.Flags().Bool("browserless", false, "--browserless") - - addRootCmd(&cobra.Command{ - Use: "logout", - Short: "Logout of your Railway account", - RunE: contextualize(handler.Logout, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "whoami", - Short: "Get the current logged in user", - RunE: contextualize(handler.Whoami, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "init", - Short: "Create a new Railway project", - PersistentPreRunE: contextualize(handler.CheckVersion, handler.Panic), - RunE: contextualize(handler.Init, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "link", - Short: "Associate existing project with current directory, may specify projectId as an argument", - PersistentPreRunE: contextualize(handler.CheckVersion, handler.Panic), - RunE: contextualize(handler.Link, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "unlink", - Short: "Disassociate project from current directory", - RunE: contextualize(handler.Unlink, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "delete [projectId]", - Short: "Delete Project, may specify projectId as an argument", - RunE: contextualize(handler.Delete, handler.Panic), - Args: cobra.MinimumNArgs(1), - }) - - addRootCmd(&cobra.Command{ - Use: "disconnect", - RunE: contextualize(handler.Unlink, handler.Panic), - Deprecated: "Please use 'railway unlink' instead", /**/ - }) - - addRootCmd(&cobra.Command{ - Use: "env", - RunE: contextualize(handler.Variables, handler.Panic), - Deprecated: "Please use 'railway variables' instead", /**/ - }) - - variablesCmd := addRootCmd(&cobra.Command{ - Use: "variables", - Aliases: []string{"vars"}, - Short: "Show variables for active environment", - RunE: contextualize(handler.Variables, handler.Panic), - }) - variablesCmd.Flags().StringP("service", "s", "", "Fetch variables accessible to a specific service") - - variablesGetCmd := &cobra.Command{ - Use: "get key", - Short: "Get the value of a variable", - RunE: contextualize(handler.VariablesGet, handler.Panic), - Args: cobra.MinimumNArgs(1), - Example: " railway variables get MY_KEY", - } - variablesCmd.AddCommand(variablesGetCmd) - variablesGetCmd.Flags().StringP("service", "s", "", "Fetch variables accessible to a specific service") - - variablesSetCmd := &cobra.Command{ - Use: "set key=value", - Short: "Create or update the value of a variable", - RunE: contextualize(handler.VariablesSet, handler.Panic), - Args: cobra.MinimumNArgs(1), - Example: " railway variables set NODE_ENV=prod NODE_VERSION=12", - } - variablesCmd.AddCommand(variablesSetCmd) - variablesSetCmd.Flags().StringP("service", "s", "", "Fetch variables accessible to a specific service") - variablesSetCmd.Flags().Bool("skip-redeploy", false, "Skip redeploying the specified service after changing the variables") - variablesSetCmd.Flags().Bool("replace", false, "Fully replace all previous variables instead of updating them") - variablesSetCmd.Flags().Bool("yes", false, "Skip all confirmation dialogs") - - variablesDeleteCmd := &cobra.Command{ - Use: "delete key", - Short: "Delete a variable", - RunE: contextualize(handler.VariablesDelete, handler.Panic), - Example: " railway variables delete MY_KEY", - } - variablesCmd.AddCommand(variablesDeleteCmd) - variablesDeleteCmd.Flags().StringP("service", "s", "", "Fetch variables accessible to a specific service") - variablesDeleteCmd.Flags().Bool("skip-redeploy", false, "Skip redeploying the specified service after changing the variables") - - addRootCmd(&cobra.Command{ - Use: "status", - Short: "Show information about the current project", - RunE: contextualize(handler.Status, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "environment", - Short: "Change the active environment", - RunE: contextualize(handler.Environment, handler.Panic), - }) - - openCmd := addRootCmd(&cobra.Command{ - Use: "open", - Short: "Open your project dashboard", - RunE: contextualize(handler.Open, handler.Panic), - }) - openCmd.AddCommand(&cobra.Command{ - Use: "metrics", - Short: "Open project metrics", - Aliases: []string{"m"}, - RunE: contextualize(handler.Open, handler.Panic), - }) - openCmd.AddCommand(&cobra.Command{ - Use: "settings", - Short: "Open project settings", - Aliases: []string{"s"}, - RunE: contextualize(handler.Open, handler.Panic), - }) - openCmd.AddCommand(&cobra.Command{ - Use: "live", - Short: "Open the deployed application", - Aliases: []string{"l"}, - RunE: contextualize(handler.OpenApp, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "list", - Short: "List all projects in your Railway account", - RunE: contextualize(handler.List, handler.Panic), - }) - - runCmd := addRootCmd(&cobra.Command{ - Use: "run", - Short: "Run a local command using variables from the active environment", - PersistentPreRunE: contextualize(handler.CheckVersion, handler.Panic), - RunE: contextualize(handler.Run, handler.Panic), - DisableFlagParsing: true, - }) - runCmd.Flags().Bool("ephemeral", false, "Run the local command in an ephemeral environment") - runCmd.Flags().String("service", "", "Run the command using variables from the specified service") - - addRootCmd(&cobra.Command{ - Use: "protect", - Short: "[EXPERIMENTAL!] Protect current branch (Actions will require confirmation)", - RunE: contextualize(handler.Protect, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "version", - Short: "Get the version of the Railway CLI", - PersistentPreRunE: contextualize(handler.CheckVersion, handler.Panic), - RunE: contextualize(handler.Version, handler.Panic), - }) - - upCmd := addRootCmd(&cobra.Command{ - Use: "up", - Short: "Upload and deploy project from the current directory", - RunE: contextualize(handler.Up, handler.Panic), - }) - upCmd.Flags().BoolP("detach", "d", false, "Detach from cloud build/deploy logs") - upCmd.Flags().StringP("environment", "e", "", "Specify an environment to up onto") - upCmd.Flags().StringP("service", "s", "", "Fetch variables accessible to a specific service") - - downCmd := addRootCmd(&cobra.Command{ - Use: "down", - Short: "Remove the most recent deployment", - RunE: contextualize(handler.Down, handler.Panic), - }) - - downCmd.Flags().StringP("environment", "e", "", "Specify an environment to delete from") - downCmd.Flags().Bool("yes", false, "Skip all confirmation dialogs") - - addRootCmd(&cobra.Command{ - Use: "logs", - Short: "View the most-recent deploy's logs", - RunE: contextualize(handler.Logs, handler.Panic), - }).Flags().Int32P("lines", "n", 0, "Output a specific number of lines") - - addRootCmd(&cobra.Command{ - Use: "docs", - Short: "Open Railway Documentation in default browser", - RunE: contextualize(handler.Docs, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "add", - Short: "Add a new plugin to your project", - RunE: contextualize(handler.Add, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "connect", - Short: "Open an interactive shell to a database", - RunE: contextualize(handler.Connect, handler.Panic), - }) - - shellCmd := addRootCmd(&cobra.Command{ - Use: "shell", - Short: "Open a subshell with Railway variables available", - RunE: contextualize(handler.Shell, handler.Panic), - }) - shellCmd.Flags().StringP("service", "s", "", "Use variables accessible to a specific service") - - addRootCmd(&cobra.Command{ - Hidden: true, - Use: "design", - Short: "Print CLI design components", - RunE: contextualize(handler.Design, handler.Panic), - }) - - addRootCmd(&cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: `To load completions: - - Bash: - - $ source <(railway completion bash) - - # To load completions for each session, execute once: - # Linux: - $ railway completion bash > /etc/bash_completion.d/railway - # macOS: - $ railway completion bash > /usr/local/etc/bash_completion.d/railway - - Zsh: - - # If shell completion is not already enabled in your environment, - # you will need to enable it. You can execute the following once: - - $ echo "autoload -U compinit; compinit" >> ~/.zshrc - - # To load completions for each session, execute once: - $ railway completion zsh > "${fpath[1]}/_railway" - - # You will need to start a new shell for this setup to take effect. - - fish: - - $ railway completion fish | source - - # To load completions for each session, execute once: - $ railway completion fish > ~/.config/fish/completions/railway.fish - - PowerShell: - - PS> railway completion powershell | Out-String | Invoke-Expression - - # To load completions for every new session, run: - PS> railway completion powershell > railway.ps1 - # and source this file from your PowerShell profile. - `, - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.ExactValidArgs(1), - RunE: contextualize(handler.Completion, handler.Panic), - }) -} - -func main() { - if _, err := os.Stat("/proc/version"); !os.IsNotExist(err) && runtime.GOOS == "windows" { - fmt.Printf("%s : Running in Non standard shell!\n Please consider using something like WSL!\n", ui.YellowText(ui.Bold("[WARNING!]").String()).String()) - } - if err := rootCmd.Execute(); err != nil { - if strings.Contains(err.Error(), "unknown command") { - suggStr := "\nS" - - suggestions := rootCmd.SuggestionsFor(os.Args[1]) - if len(suggestions) > 0 { - suggStr = fmt.Sprintf(" Did you mean \"%s\"?\nIf not, s", suggestions[0]) - } - - fmt.Println(fmt.Sprintf("Unknown command \"%s\" for \"%s\".%s"+ - "ee \"railway --help\" for available commands.", - os.Args[1], rootCmd.CommandPath(), suggStr)) - } else { - fmt.Println(err) - } - os.Exit(1) - } -} diff --git a/npm-install/config.js b/npm-install/config.js deleted file mode 100644 index 3307e19..0000000 --- a/npm-install/config.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Global configuration - */ -export const CONFIG = { - /** - * The name of the binary - * @type {string} - */ - name: "railway", - - /** - * Where to save the unzipped files - * @type {string} - */ - path: "./bin", - - /** - * The URL to download the binary form - * - * - `{{arch}}` is one of the Golang achitectures listed below - * - `{{bin_name}}` is the name declared above - * - `{{platform}}` is one of the Golang platforms listed below - * - `{{version}}` is the version number as `0.0.0` (taken from package.json) - * - * @type {string} - */ - url: "https://github.com/railwayapp/cli/releases/download/v{{version}}/{{bin_name}}_{{version}}_{{platform}}_{{arch}}.tar.gz", -}; - -/** - * Mapping from Node's `process.arch` to Golang's `$GOARCH` - */ -export const ARCH_MAPPING = { - ia32: "386", - x64: "amd64", - arm64: "arm64", -}; - -/** - * Mapping between Node's `process.platform` to Golang's - */ -export const PLATFORM_MAPPING = { - darwin: "darwin", - linux: "linux", - win32: "windows", - // freebsd: "freebsd", -}; diff --git a/npm-install/postinstall.js b/npm-install/postinstall.js deleted file mode 100644 index b0dea50..0000000 --- a/npm-install/postinstall.js +++ /dev/null @@ -1,56 +0,0 @@ -import { createWriteStream } from "fs"; -import * as fs from "fs/promises"; -import fetch from "node-fetch"; -import { pipeline } from "stream/promises"; -import tar from "tar"; -import { execSync } from "child_process"; - -import { ARCH_MAPPING, CONFIG, PLATFORM_MAPPING } from "./config.js"; - -async function install() { - if (process.platform === "android") { - console.log("Installing, this may take a few minutes..."); - const cmd = - "pkg upgrade && pkg install golang git -y && git clone https://github.com/railwayapp/cli.git && cd cli/ && go build -o $PREFIX/bin/railway"; - execSync(cmd, { encoding: "utf-8" }); - console.log("Installation successful!"); - return; - } - const packageJson = await fs.readFile("package.json").then(JSON.parse); - let version = packageJson.version; - - if (typeof version !== "string") { - throw new Error("Missing version in package.json"); - } - - if (version[0] === "v") version = version.slice(1); - - // Fetch Static Config - let { name: binName, path: binPath, url } = CONFIG; - - url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]); - url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]); - url = url.replace(/{{version}}/g, version); - url = url.replace(/{{bin_name}}/g, binName); - - const response = await fetch(url); - if (!response.ok) { - throw new Error("Failed fetching the binary: " + response.statusText); - } - - const tarFile = "downloaded.tar.gz"; - - await fs.mkdir(binPath, { recursive: true }); - await pipeline(response.body, createWriteStream(tarFile)); - await tar.x({ file: tarFile, cwd: binPath }); - await fs.rm(tarFile); -} - -install() - .then(async () => { - process.exit(0); - }) - .catch(async (err) => { - console.error(err); - process.exit(1); - }); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6480fba..0000000 --- a/package-lock.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "name": "@railway/cli", - "version": "0.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "@railway/cli", - "version": "0.0.0", - "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "node-fetch": "^3.1.0", - "tar": "^6.1.11" - }, - "bin": { - "railway": "bin/railway.js" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", - "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/fetch-blob": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz", - "integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", - "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/node-fetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.0.tgz", - "integrity": "sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.2", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", - "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "data-uri-to-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", - "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" - }, - "fetch-blob": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz", - "integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==", - "requires": { - "web-streams-polyfill": "^3.0.3" - } - }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "requires": { - "fetch-blob": "^3.1.2" - } - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", - "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "node-fetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.0.tgz", - "integrity": "sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.2", - "formdata-polyfill": "^4.0.10" - } - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - } - } - }, - "web-streams-polyfill": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", - "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 3c966c9..0000000 --- a/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@railway/cli", - "version": "0.0.0", - "description": "Develop and deploy code with zero configuration", - "type": "module", - "author": "Jake Runzer", - "license": "ISC", - "homepage": "https://github.com/railwayapp/cli/blob/master/README.md", - "repository": { - "type": "git", - "url": "https://github.com/railwayapp/cli.git" - }, - "engines": { - "node": ">=16.0.0" - }, - "scripts": { - "postinstall": "node ./npm-install/postinstall.js" - }, - "bin": { - "railway": "bin/railway.js" - }, - "files": [ - "npm-install" - ], - "dependencies": { - "node-fetch": "^3.1.0", - "tar": "^6.1.11" - } -} diff --git a/random/main.go b/random/main.go deleted file mode 100644 index 8ccaba2..0000000 --- a/random/main.go +++ /dev/null @@ -1,78 +0,0 @@ -package random - -import ( - cryptoRand "crypto/rand" - "encoding/base64" - "fmt" - "math/rand" - mathRand "math/rand" - "net" - "time" -) - -type Randomizer struct { - mathRand.Rand -} - -// Bytes returns securely generated random bytes. -// It will return an error if the system's secure random -// number generator fails to function correctly, in which -// case the caller should not continue. -func (r *Randomizer) Bytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := cryptoRand.Read(b) - // Note that err == nil only if we read len(b) bytes. - if err != nil { - return nil, err - } - - return b, nil -} - -// String returns a URL-safe, base64 encoded -// securely generated random string. -func (r *Randomizer) String(s int) (string, error) { - b, err := r.Bytes(s) - return base64.URLEncoding.EncodeToString(b), err -} - -// Number generates a number between 0 and n -func (r *Randomizer) Number(n int) int { - return mathRand.Intn(n) -} - -// NumberBetween returns a random number between n and m -func (r *Randomizer) NumberBetween(n int, m int) int { - return mathRand.Intn(m-n) + n -} - -// Port asks the kernel for an available port -func (r *Randomizer) Port() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - return 0, err - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, err - } - defer l.Close() - return l.Addr().(*net.TCPAddr).Port, nil -} - -// Code returns a random code -func (r *Randomizer) Code() string { - return fmt.Sprintf("%016d", rand.Int63n(1e16)) -} - -// New returns a preseeded randomizer -func New() *Randomizer { - randomizer := mathRand.New(mathRand.NewSource(time.Now().UTC().UnixNano())) - if randomizer == nil { - panic("Failed to start random number generator") - } - return &Randomizer{ - *randomizer, - } -} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..83d8e1b --- /dev/null +++ b/shell.nix @@ -0,0 +1,29 @@ +{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/313b84933167.tar.gz") { + overlays = [ + (import (fetchTarball "https://github.com/oxalica/rust-overlay/archive/d0dc81ffe8ea.tar.gz")) + ]; + } +}: + +let + rust = with pkgs; + rust-bin.stable.latest.minimal; + basePkgs = with pkgs; + [ + cmake + rust + act + cargo-zigbuild + ]; + + # macOS only + inputs = with pkgs; + basePkgs ++ lib.optionals stdenv.isDarwin + (with darwin.apple_sdk.frameworks; [ + Security + ]); +in +pkgs.mkShell +{ + buildInputs = inputs; +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..7543aaf --- /dev/null +++ b/src/client.rs @@ -0,0 +1,69 @@ +use graphql_client::{GraphQLQuery, Response}; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + Client, +}; + +use crate::{commands::Environment, config::Configs, consts}; +use anyhow::{bail, Result}; + +pub struct GQLClient; + +impl GQLClient { + pub fn new_authorized(configs: &Configs) -> Result { + let mut headers = HeaderMap::new(); + if let Some(token) = &Configs::get_railway_token() { + headers.insert("project-access-token", HeaderValue::from_str(token)?); + } else if let Some(token) = &Configs::get_railway_api_token() { + headers.insert( + "authorization", + HeaderValue::from_str(&format!("Bearer {token}"))?, + ); + } else if let Some(token) = &configs.root_config.user.token { + if token.is_empty() { + bail!("Unauthorized. Please login with `railway login`") + } + headers.insert( + "authorization", + HeaderValue::from_str(&format!("Bearer {token}"))?, + ); + } else { + bail!("Unauthorized. Please login with `railway login`") + } + headers.insert( + "x-source", + HeaderValue::from_static(consts::get_user_agent()), + ); + let client = Client::builder() + .danger_accept_invalid_certs(matches!(Configs::get_environment_id(), Environment::Dev)) + .user_agent(consts::get_user_agent()) + .default_headers(headers) + .build()?; + Ok(client) + } + + pub fn new_unauthorized() -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + "x-source", + HeaderValue::from_static(consts::get_user_agent()), + ); + let client = Client::builder() + .danger_accept_invalid_certs(matches!(Configs::get_environment_id(), Environment::Dev)) + .user_agent(consts::get_user_agent()) + .default_headers(headers) + .build()?; + Ok(client) + } +} + +pub async fn post_graphql( + client: &reqwest::Client, + url: U, + variables: Q::Variables, +) -> Result, reqwest::Error> { + let body = Q::build_query(variables); + let reqwest_response = client.post(url).json(&body).send().await?; + + reqwest_response.json().await +} diff --git a/src/commands/add.rs b/src/commands/add.rs new file mode 100644 index 0000000..53624c6 --- /dev/null +++ b/src/commands/add.rs @@ -0,0 +1,128 @@ +use std::time::Duration; + +use anyhow::bail; +use clap::ValueEnum; +use is_terminal::IsTerminal; + +use crate::consts::{PLUGINS, TICK_STRING}; + +use super::{queries::project_plugins::PluginType, *}; + +/// Add a new plugin to your project +#[derive(Parser)] +pub struct Args { + /// The name of the plugin to add + #[arg(short, long, value_enum)] + plugin: Vec, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + let render_config = Configs::get_render_config(); + + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project_plugins::Variables { + id: linked_project.project.clone(), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + + let project_plugins: Vec<_> = body + .project + .plugins + .edges + .iter() + .map(|p| plugin_enum_to_string(&p.node.name)) + .collect(); + + let filtered_plugins: Vec<_> = PLUGINS + .iter() + .map(|p| p.to_string()) + .filter(|plugin| !project_plugins.contains(&plugin.to_string())) + .collect(); + + let selected = if !std::io::stdout().is_terminal() || !args.plugin.is_empty() { + if args.plugin.is_empty() { + bail!("No plugins specified"); + } + let filtered: Vec<_> = args + .plugin + .iter() + .map(clap_plugin_enum_to_plugin_enum) + .map(|p| plugin_enum_to_string(&p)) + .filter(|plugin| !project_plugins.contains(&plugin.to_string())) + .collect(); + + if filtered.is_empty() { + bail!("Plugins already exist"); + } + + filtered + } else { + inquire::MultiSelect::new("Select plugins to add", filtered_plugins) + .with_render_config(render_config) + .prompt()? + }; + + if selected.is_empty() { + bail!("No plugins selected"); + } + + for plugin in selected { + let vars = mutations::plugin_create::Variables { + project_id: linked_project.project.clone(), + name: plugin.to_lowercase(), + }; + if !std::io::stdout().is_terminal() { + println!("Creating {}...", plugin); + post_graphql::(&client, configs.get_backboard(), vars) + .await?; + } else { + let spinner = indicatif::ProgressBar::new_spinner() + .with_style( + indicatif::ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg}")?, + ) + .with_message(format!("Creating {plugin}...")); + spinner.enable_steady_tick(Duration::from_millis(100)); + post_graphql::(&client, configs.get_backboard(), vars) + .await?; + spinner.finish_with_message(format!("Created {plugin}")); + } + } + + Ok(()) +} + +fn plugin_enum_to_string(plugin: &PluginType) -> String { + match plugin { + PluginType::postgresql => "PostgreSQL".to_owned(), + PluginType::mysql => "MySQL".to_owned(), + PluginType::redis => "Redis".to_owned(), + PluginType::mongodb => "MongoDB".to_owned(), + PluginType::Other(other) => other.to_owned(), + } +} + +#[derive(ValueEnum, Clone, Debug)] +enum ClapPluginEnum { + Postgresql, + Mysql, + Redis, + Mongodb, +} + +fn clap_plugin_enum_to_plugin_enum(clap_plugin_enum: &ClapPluginEnum) -> PluginType { + match clap_plugin_enum { + ClapPluginEnum::Postgresql => PluginType::postgresql, + ClapPluginEnum::Mysql => PluginType::mysql, + ClapPluginEnum::Redis => PluginType::redis, + ClapPluginEnum::Mongodb => PluginType::mongodb, + } +} diff --git a/src/commands/completion.rs b/src/commands/completion.rs new file mode 100644 index 0000000..b40a28f --- /dev/null +++ b/src/commands/completion.rs @@ -0,0 +1,21 @@ +use super::*; + +use clap::CommandFactory; +use clap_complete::{generate, Shell}; +use std::io; + +/// Generate completion script +#[derive(Parser)] +pub struct Args { + shell: Shell, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + generate( + args.shell, + &mut crate::Args::command(), + "railway", + &mut io::stdout(), + ); + Ok(()) +} diff --git a/src/commands/delete.rs b/src/commands/delete.rs new file mode 100644 index 0000000..bad84b9 --- /dev/null +++ b/src/commands/delete.rs @@ -0,0 +1,113 @@ +use std::time::Duration; + +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::consts::{ABORTED_BY_USER, TICK_STRING}; + +use super::{queries::project_plugins::PluginType, *}; + +/// Delete plugins from a project +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + if !std::io::stdout().is_terminal() { + bail!("Cannot delete plugins in non-interactive mode"); + } + let configs = Configs::new()?; + let render_config = Configs::get_render_config(); + + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let is_two_factor_enabled = { + let vars = queries::two_factor_info::Variables {}; + + let res = post_graphql::(&client, configs.get_backboard(), vars) + .await?; + let info = res.data.context("No data")?.two_factor_info; + + info.is_verified + }; + + if is_two_factor_enabled { + let token = inquire::Text::new("Enter your 2FA code") + .with_render_config(render_config) + .prompt()?; + let vars = mutations::validate_two_factor::Variables { token }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars) + .await?; + let valid = res.data.context("No data")?.two_factor_info_validate; + + if !valid { + bail!("Invalid 2FA code"); + } + } + + let vars = queries::project_plugins::Variables { + id: linked_project.project.clone(), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + let nodes = body.project.plugins.edges; + let project_plugins: Vec<_> = nodes + .iter() + .map(|p| plugin_enum_to_string(&p.node.name)) + .collect(); + + let selected = inquire::MultiSelect::new("Select plugins to delete", project_plugins) + .with_render_config(render_config) + .prompt()?; + + for plugin in selected { + let id = nodes + .iter() + .find(|p| plugin_enum_to_string(&p.node.name) == plugin) + .context("Plugin not found")? + .node + .id + .clone(); + + let vars = mutations::plugin_delete::Variables { id }; + + let confirmed = + inquire::Confirm::new(format!("Are you sure you want to delete {plugin}?").as_str()) + .with_default(false) + .with_render_config(render_config) + .prompt()?; + + if !confirmed { + bail!(ABORTED_BY_USER) + } + + let spinner = indicatif::ProgressBar::new_spinner() + .with_style( + indicatif::ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg}")?, + ) + .with_message(format!("Deleting {plugin}...")); + spinner.enable_steady_tick(Duration::from_millis(100)); + + post_graphql::(&client, configs.get_backboard(), vars).await?; + + spinner.finish_with_message(format!("Deleted {plugin}")); + } + Ok(()) +} + +fn plugin_enum_to_string(plugin: &PluginType) -> String { + match plugin { + PluginType::postgresql => "PostgreSQL".to_owned(), + PluginType::mysql => "MySQL".to_owned(), + PluginType::redis => "Redis".to_owned(), + PluginType::mongodb => "MongoDB".to_owned(), + PluginType::Other(other) => other.to_owned(), + } +} diff --git a/src/commands/docs.rs b/src/commands/docs.rs new file mode 100644 index 0000000..778188b --- /dev/null +++ b/src/commands/docs.rs @@ -0,0 +1,27 @@ +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::consts::{ABORTED_BY_USER, NON_INTERACTIVE_FAILURE}; + +use super::*; + +/// Open Railway Documentation in default browser +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + if !std::io::stdout().is_terminal() { + bail!(NON_INTERACTIVE_FAILURE); + } + let confirm = inquire::Confirm::new("Open the browser") + .with_default(true) + .with_render_config(Configs::get_render_config()) + .prompt()?; + + if !confirm { + bail!(ABORTED_BY_USER); + } + + ::open::that("https://docs.railway.app/")?; + Ok(()) +} diff --git a/src/commands/domain.rs b/src/commands/domain.rs new file mode 100644 index 0000000..da0c540 --- /dev/null +++ b/src/commands/domain.rs @@ -0,0 +1,115 @@ +use std::time::Duration; + +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::consts::TICK_STRING; + +use super::*; + +/// Generates a domain for a service if there is not a railway provided domain +// Checks if the user is linked to a service, if not, it will generate a domain for the default service +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + + if body.project.services.edges.is_empty() { + bail!("No services found for project"); + } + + // If there is only one service, it will generate a domain for that service + let service = if body.project.services.edges.len() == 1 { + body.project.services.edges[0].node.clone().id + } else { + let Some(service) = linked_project.service.clone() else { + bail!("No service linked. Run `railway service` to link to a service"); + }; + if body + .project + .services + .edges + .iter() + .any(|s| s.node.id == service) + { + service + } else { + bail!("Service not found! Run `railway service` to link to a service"); + } + }; + + let vars = queries::domains::Variables { + project_id: linked_project.project.clone(), + environment_id: linked_project.environment.clone(), + service_id: service.clone(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res + .data + .context("Failed to retrieve to get domains for service.")?; + + let domain = body.domains; + if !(domain.service_domains.is_empty() || domain.custom_domains.is_empty()) { + bail!("Domain already exists on service"); + } + + let vars = mutations::service_domain_create::Variables { + service_id: service, + environment_id: linked_project.environment.clone(), + }; + + if !std::io::stdout().is_terminal() { + println!("Creating domain..."); + + let res = post_graphql::( + &client, + configs.get_backboard(), + vars, + ) + .await?; + + let body = res.data.context("Failed to create service domain.")?; + let domain = body.service_domain_create.domain; + + println!("Service Domain created: {}", domain.bold()); + } else { + let spinner = indicatif::ProgressBar::new_spinner() + .with_style( + indicatif::ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg}")?, + ) + .with_message("Creating domain..."); + spinner.enable_steady_tick(Duration::from_millis(100)); + + let res = post_graphql::( + &client, + configs.get_backboard(), + vars, + ) + .await?; + + let body = res.data.context("Failed to create service domain.")?; + let domain = body.service_domain_create.domain; + + spinner.finish_and_clear(); + + println!("Service Domain created: {}", domain.bold()); + } + + Ok(()) +} diff --git a/src/commands/environment.rs b/src/commands/environment.rs new file mode 100644 index 0000000..a72af2b --- /dev/null +++ b/src/commands/environment.rs @@ -0,0 +1,82 @@ +use std::fmt::Display; + +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::{interact_or, util::prompt::prompt_options}; + +use super::{queries::project::ProjectProjectEnvironmentsEdgesNode, *}; + +/// Change the active environment +#[derive(Parser)] +pub struct Args { + /// The environment to link to + environment: Option, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let mut configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res + .data + .context("Failed to get environments (query project)")?; + + let environments: Vec<_> = body + .project + .environments + .edges + .iter() + .map(|env| Environment(&env.node)) + .collect(); + + let environment = match args.environment { + // If the environment is specified, find it in the list of environments + Some(environment) => { + let environment = environments + .iter() + .find(|env| env.0.id == environment || env.0.name == environment) + .context("Environment not found")?; + environment.clone() + } + // If the environment is not specified, prompt the user to select one + None => { + interact_or!("Environment must be specified when not running in a terminal"); + let environment = if environments.len() == 1 { + match environments.first() { + // Project has only one environment, so use that one + Some(environment) => environment.clone(), + // Project has no environments, so bail + None => bail!("Project has no environments"), + } + } else { + // Project has multiple environments, so prompt the user to select one + prompt_options("Select an environment", environments)? + }; + environment + } + }; + + configs.link_project( + linked_project.project.clone(), + linked_project.name.clone(), + environment.0.id.clone(), + Some(environment.0.name.clone()), + )?; + configs.write()?; + Ok(()) +} + +#[derive(Debug, Clone)] +struct Environment<'a>(&'a ProjectProjectEnvironmentsEdgesNode); + +impl<'a> Display for Environment<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} diff --git a/src/commands/init.rs b/src/commands/init.rs new file mode 100644 index 0000000..40ff790 --- /dev/null +++ b/src/commands/init.rs @@ -0,0 +1,139 @@ +use std::fmt::Display; + +use super::{queries::user_projects::UserProjectsMeTeamsEdgesNode, *}; + +/// Create a new project +#[derive(Parser)] +#[clap(alias = "new")] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + let mut configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + + let vars = queries::user_projects::Variables {}; + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res.data.context("Failed to get user (query me)")?; + + let teams: Vec<_> = body.me.teams.edges.iter().map(|team| &team.node).collect(); + let team_names = get_team_names(teams); + let team = prompt_team(team_names)?; + let project_name = prompt_project_name()?; + + let team_id = match team { + Team::Team(team) => Some(team.id.clone()), + _ => None, + }; + + let vars = mutations::project_create::Variables { + name: Some(project_name), + description: None, + team_id, + }; + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res + .data + .context("Failed to create project (mutation projectCreate)")?; + + let environment = body + .project_create + .environments + .edges + .first() + .context("No environments")? + .node + .clone(); + + configs.link_project( + body.project_create.id.clone(), + Some(body.project_create.name.clone()), + environment.id, + Some(environment.name), + )?; + configs.write()?; + + println!( + "{} {} on {}", + "Created project".green().bold(), + body.project_create.name.bold(), + team + ); + + println!( + "{}", + format!( + "https://{}/project/{}", + configs.get_host(), + body.project_create.id + ) + .bold() + .underline() + ); + Ok(()) +} + +fn get_team_names(teams: Vec<&UserProjectsMeTeamsEdgesNode>) -> Vec { + let mut team_names = teams + .iter() + .map(|team| Team::Team(team)) + .collect::>(); + team_names.insert(0, Team::Personal); + team_names +} + +fn prompt_team(teams: Vec) -> Result { + // If there is only the personal team, return None + if teams.len() == 1 { + return Ok(Team::Personal); + } + let select = inquire::Select::new("Team", teams); + let team = select + .with_render_config(Configs::get_render_config()) + .prompt()?; + Ok(team) +} + +fn prompt_project_name() -> Result { + // Need a custom inquire prompt here, because of the formatter + let maybe_name = inquire::Text::new("Project Name") + .with_formatter(&|s| { + if s.is_empty() { + "Will be randomly generated".to_string() + } else { + s.to_string() + } + }) + .with_placeholder("my-first-project") + .with_help_message("Leave blank to generate a random name") + .with_render_config(Configs::get_render_config()) + .prompt()?; + + // If name is empty, generate a random name + let name = match maybe_name.as_str() { + "" => { + use names::Generator; + let mut generator = Generator::default(); + generator.next().context("Failed to generate name")? + } + _ => maybe_name, + }; + + Ok(name) +} + +#[derive(Debug, Clone)] +enum Team<'a> { + Team(&'a UserProjectsMeTeamsEdgesNode), + Personal, +} + +impl<'a> Display for Team<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Team::Team(team) => write!(f, "{}", team.name), + Team::Personal => write!(f, "{}", "Personal".bold()), + } + } +} diff --git a/src/commands/link.rs b/src/commands/link.rs new file mode 100644 index 0000000..41d5b78 --- /dev/null +++ b/src/commands/link.rs @@ -0,0 +1,255 @@ +use std::fmt::Display; + +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::{ + commands::queries::user_projects::UserProjectsMeTeamsEdgesNode, consts::PROJECT_NOT_FOUND, + util::prompt::prompt_options, +}; + +use super::{ + queries::{ + project::ProjectProjectEnvironmentsEdgesNode, + projects::{ProjectsProjectsEdgesNode, ProjectsProjectsEdgesNodeEnvironmentsEdgesNode}, + user_projects::{ + UserProjectsMeProjectsEdgesNode, UserProjectsMeProjectsEdgesNodeEnvironmentsEdgesNode, + }, + }, + *, +}; + +/// Associate existing project with current directory, may specify projectId as an argument +#[derive(Parser)] +pub struct Args { + #[clap(long)] + /// Environment to link to + environment: Option, + + /// Project ID to link to + project_id: Option, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let mut configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + + if let Some(project_id) = args.project_id { + let vars = queries::project::Variables { id: project_id }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res.data.context(PROJECT_NOT_FOUND)?; + + let environment = if let Some(environment_name_or_id) = args.environment { + let environment = body + .project + .environments + .edges + .iter() + .find(|env| { + env.node.name == environment_name_or_id || env.node.id == environment_name_or_id + }) + .context("Environment not found")?; + ProjectEnvironment(&environment.node) + } else if !std::io::stdout().is_terminal() { + bail!("Environment must be provided when not running in a terminal"); + } else if body.project.environments.edges.len() == 1 { + ProjectEnvironment(&body.project.environments.edges[0].node) + } else { + prompt_options( + "Select an environment", + body.project + .environments + .edges + .iter() + .map(|env| ProjectEnvironment(&env.node)) + .collect(), + )? + }; + + configs.link_project( + body.project.id.clone(), + Some(body.project.name.clone()), + environment.0.id.clone(), + Some(environment.0.name.clone()), + )?; + configs.write()?; + return Ok(()); + } else if !std::io::stdout().is_terminal() { + bail!("Project must be provided when not running in a terminal"); + } + + let vars = queries::user_projects::Variables {}; + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res.data.context("Failed to retrieve response body")?; + + let mut personal_projects: Vec<_> = body + .me + .projects + .edges + .iter() + .map(|project| &project.node) + .collect(); + personal_projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + + let personal_project_names = personal_projects + .iter() + .map(|project| PersonalProject(project)) + .collect::>(); + + let teams: Vec<_> = body.me.teams.edges.iter().map(|team| &team.node).collect(); + + if teams.is_empty() { + let (project, environment) = prompt_personal_projects(personal_project_names)?; + configs.link_project( + project.0.id.clone(), + Some(project.0.name.clone()), + environment.0.id.clone(), + Some(environment.0.name.clone()), + )?; + configs.write()?; + return Ok(()); + } + + let mut team_names = teams + .iter() + .map(|team| Team::Team(team)) + .collect::>(); + team_names.insert(0, Team::Personal); + + let team = prompt_options("Select a team", team_names)?; + match team { + Team::Personal => { + let (project, environment) = prompt_personal_projects(personal_project_names)?; + configs.link_project( + project.0.id.clone(), + Some(project.0.name.clone()), + environment.0.id.clone(), + Some(environment.0.name.clone()), + )?; + } + Team::Team(team) => { + let vars = queries::projects::Variables { + team_id: Some(team.id.clone()), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars) + .await?; + + let body = res.data.context("Failed to retrieve response body")?; + let mut projects: Vec<_> = body + .projects + .edges + .iter() + .map(|project| &project.node) + .collect(); + projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + + let project_names = projects + .iter() + .map(|project| Project(project)) + .collect::>(); + let (project, environment) = prompt_team_projects(project_names)?; + configs.link_project( + project.0.id.clone(), + Some(project.0.name.clone()), + environment.0.id.clone(), + Some(environment.0.name.clone()), + )?; + } + } + + configs.write()?; + + Ok(()) +} + +fn prompt_team_projects(project_names: Vec) -> Result<(Project, Environment)> { + let project = prompt_options("Select a project", project_names)?; + let environments = project + .0 + .environments + .edges + .iter() + .map(|env| Environment(&env.node)) + .collect(); + let environment = prompt_options("Select an environment", environments)?; + Ok((project, environment)) +} + +fn prompt_personal_projects( + personal_project_names: Vec, +) -> Result<(PersonalProject, PersonalEnvironment)> { + let project = prompt_options("Select a project", personal_project_names)?; + let environments = project + .0 + .environments + .edges + .iter() + .map(|env| PersonalEnvironment(&env.node)) + .collect(); + let environment = prompt_options("Select an environment", environments)?; + Ok((project, environment)) +} + +#[derive(Debug, Clone)] +struct PersonalProject<'a>(&'a UserProjectsMeProjectsEdgesNode); + +impl<'a> Display for PersonalProject<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} + +#[derive(Debug, Clone)] +struct PersonalEnvironment<'a>(&'a UserProjectsMeProjectsEdgesNodeEnvironmentsEdgesNode); + +impl<'a> Display for PersonalEnvironment<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} + +#[derive(Debug, Clone)] +struct Project<'a>(&'a ProjectsProjectsEdgesNode); + +impl<'a> Display for Project<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} + +#[derive(Debug, Clone)] +struct Environment<'a>(&'a ProjectsProjectsEdgesNodeEnvironmentsEdgesNode); + +impl<'a> Display for Environment<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} + +#[derive(Debug, Clone)] +enum Team<'a> { + Team(&'a UserProjectsMeTeamsEdgesNode), + Personal, +} + +impl<'a> Display for Team<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Team::Team(team) => write!(f, "{}", team.name), + Team::Personal => write!(f, "{}", "Personal".bold()), + } + } +} + +#[derive(Debug, Clone)] +struct ProjectEnvironment<'a>(&'a ProjectProjectEnvironmentsEdgesNode); + +impl<'a> Display for ProjectEnvironment<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} diff --git a/src/commands/list.rs b/src/commands/list.rs new file mode 100644 index 0000000..1fc9410 --- /dev/null +++ b/src/commands/list.rs @@ -0,0 +1,105 @@ +use serde::Serialize; + +use super::{ + queries::{ + projects::ProjectsProjectsEdgesNode, user_projects::UserProjectsMeProjectsEdgesNode, + }, + *, +}; + +/// List all projects in your Railway account +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await.ok(); + + let vars = queries::user_projects::Variables {}; + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res.data.context("Failed to get user (query me)")?; + + let mut personal_projects: Vec<_> = body + .me + .projects + .edges + .iter() + .map(|project| &project.node) + .collect(); + // Sort by most recently updated (matches dashboard behavior) + personal_projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + + let mut all_projects: Vec<_> = personal_projects + .iter() + .map(|project| Project::Me((*project).clone())) + .collect(); + + let teams: Vec<_> = body.me.teams.edges.iter().map(|team| &team.node).collect(); + if !json { + println!("{}", "Personal".bold()); + for project in &personal_projects { + let project_name = if linked_project.is_some() + && project.id == linked_project.as_ref().unwrap().project + { + project.name.purple().bold() + } else { + project.name.white() + }; + println!(" {project_name}"); + } + } + + for team in teams { + if !json { + println!(); + println!("{}", team.name.bold()); + } + { + let vars = queries::projects::Variables { + team_id: Some(team.id.clone()), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars) + .await?; + + let body = res.data.context("Failed to retrieve response body")?; + let mut projects: Vec<_> = body + .projects + .edges + .iter() + .map(|project| &project.node) + .collect(); + projects.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)); + let mut team_projects: Vec<_> = projects + .iter() + .map(|project| Project::Team((*project).clone())) + .collect(); + all_projects.append(&mut team_projects); + if !json { + for project in &projects { + let project_name = if linked_project.is_some() + && project.id == linked_project.as_ref().unwrap().project + { + project.name.purple().bold() + } else { + project.name.white() + }; + println!(" {project_name}"); + } + } + } + } + if json { + println!("{}", serde_json::to_string_pretty(&all_projects)?); + } + Ok(()) +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +enum Project { + Me(UserProjectsMeProjectsEdgesNode), + Team(ProjectsProjectsEdgesNode), +} diff --git a/src/commands/login.rs b/src/commands/login.rs new file mode 100644 index 0000000..bc546c5 --- /dev/null +++ b/src/commands/login.rs @@ -0,0 +1,256 @@ +use std::{net::SocketAddr, time::Duration}; + +use crate::consts::{ABORTED_BY_USER, TICK_STRING}; + +use super::*; +use anyhow::bail; +use http_body_util::Full; +use hyper::{body::Bytes, server::conn::http1, service::service_fn, Request, Response}; +use is_terminal::IsTerminal; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use tokio::net::TcpListener; + +/// Login to your Railway account +#[derive(Parser)] +pub struct Args { + /// Browserless login + #[clap(short, long)] + browserless: bool, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + if !std::io::stdout().is_terminal() { + bail!("Cannot login in non-interactive mode"); + } + + let mut configs = Configs::new()?; + let render_config = Configs::get_render_config(); + + if args.browserless { + return browserless_login().await; + } + + let confirm = inquire::Confirm::new("Open the browser") + .with_default(true) + .with_render_config(render_config) + .prompt()?; + + if !confirm { + bail!(ABORTED_BY_USER); + } + + let port = rand::thread_rng().gen_range(50000..60000); + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + + let listener = TcpListener::bind(addr).await?; + + let (tx, mut rx) = tokio::sync::mpsc::channel::(1); + let hello = move |req: Request| { + let tx = tx.clone(); + async move { + let configs = Configs::new()?; + let hostname = format!("https://{}", configs.get_host()); + if req.method() == hyper::Method::GET { + let mut pairs = req.uri().query().context("No query")?.split('&'); + + let token = pairs + .next() + .context("No token")? + .split('=') + .nth(1) + .context("No token")? + .to_owned(); + + tx.send(token).await?; + let res = LoginResponse { + status: "Ok".to_owned(), + error: "".to_owned(), + }; + let res_json = serde_json::to_string(&res)?; + let mut response = Response::new(Full::from(res_json)); + response.headers_mut().insert( + "Content-Type", + hyper::header::HeaderValue::from_static("application/json"), + ); + response.headers_mut().insert( + "Access-Control-Allow-Origin", + hyper::header::HeaderValue::from_str(hostname.as_str()).unwrap(), + ); + Ok::>, anyhow::Error>(response) + } else { + let mut response = Response::default(); + response.headers_mut().insert( + "Access-Control-Allow-Methods", + hyper::header::HeaderValue::from_static("GET, HEAD, PUT, PATCH, POST, DELETE"), + ); + response.headers_mut().insert( + "Access-Control-Allow-Headers", + hyper::header::HeaderValue::from_static("*"), + ); + response.headers_mut().insert( + "Access-Control-Allow-Origin", + hyper::header::HeaderValue::from_str(hostname.as_str()).unwrap(), + ); + response.headers_mut().insert( + "Content-Length", + hyper::header::HeaderValue::from_static("0"), + ); + *response.status_mut() = hyper::StatusCode::NO_CONTENT; + Ok::>, anyhow::Error>(response) + } + } + }; + if ::open::that(generate_cli_login_url(port)?).is_err() { + return browserless_login().await; + } + let spinner = indicatif::ProgressBar::new_spinner() + .with_style( + indicatif::ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg}")?, + ) + .with_message("Waiting for login..."); + spinner.enable_steady_tick(Duration::from_millis(100)); + + let (stream, _) = listener.accept().await?; + + // Intentionally not awaiting this task, so that we exit after a single request + tokio::task::spawn(async move { + http1::Builder::new() + .serve_connection(stream, service_fn(hello)) + .await?; + Ok::<_, anyhow::Error>(()) + }); + + let token = rx.recv().await.context("No token received")?; + configs.root_config.user.token = Some(token); + configs.write()?; + + let client = GQLClient::new_authorized(&configs)?; + let vars = queries::user_meta::Variables {}; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + let me = res.data.context("No data")?.me; + + spinner.finish_and_clear(); + println!( + "Logged in as {} ({})", + me.name.context("No name")?.bold(), + me.email + ); + + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize)] +struct LoginResponse { + status: String, + error: String, +} + +fn get_random_numeric_code(length: usize) -> String { + let mut rng = rand::thread_rng(); + + std::iter::from_fn(|| rng.gen_range(0..10).to_string().chars().next()) + .take(length) + .collect() +} + +fn generate_login_payload(port: u16) -> Result { + let code = get_random_numeric_code(32); + let hostname_os = hostname::get()?; + let hostname = hostname_os.to_str().context("Invalid hostname")?; + let payload = format!("port={port}&code={code}&hostname={hostname}"); + Ok(payload) +} + +fn generate_cli_login_url(port: u16) -> Result { + use base64::{ + alphabet::URL_SAFE, + engine::{GeneralPurpose, GeneralPurposeConfig}, + Engine, + }; + let payload = generate_login_payload(port)?; + let configs = Configs::new()?; + let hostname = configs.get_host(); + + let engine = GeneralPurpose::new(&URL_SAFE, GeneralPurposeConfig::new()); + let encoded_payload = engine.encode(payload.as_bytes()); + + let url = format!("https://{hostname}/cli-login?d={encoded_payload}"); + Ok(url) +} + +async fn browserless_login() -> Result<()> { + let mut configs = Configs::new()?; + let hostname = hostname::get()?; + let hostname = hostname.to_str().context("Invalid hostname")?; + + println!("{}", "Browserless Login".bold()); + let client = GQLClient::new_unauthorized()?; + let vars = mutations::login_session_create::Variables {}; + let res = + post_graphql::(&client, configs.get_backboard(), vars) + .await?; + let word_code = res.data.context("No data")?.login_session_create; + + use base64::{ + alphabet::URL_SAFE, + engine::{GeneralPurpose, GeneralPurposeConfig}, + Engine, + }; + let payload = format!("wordCode={word_code}&hostname={hostname}"); + + let engine = GeneralPurpose::new(&URL_SAFE, GeneralPurposeConfig::new()); + let encoded_payload = engine.encode(payload.as_bytes()); + let hostname = configs.get_host(); + println!( + "Please visit:\n {}", + format!("https://{hostname}/cli-login?d={encoded_payload}") + .bold() + .underline() + ); + println!("Your pairing code is: {}", word_code.bold().purple()); + let spinner = indicatif::ProgressBar::new_spinner() + .with_style( + indicatif::ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg}")?, + ) + .with_message("Waiting for login..."); + spinner.enable_steady_tick(Duration::from_millis(100)); + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + let vars = mutations::login_session_consume::Variables { + code: word_code.clone(), + }; + let res = post_graphql::( + &client, + configs.get_backboard(), + vars, + ) + .await?; + if let Some(token) = res.data.context("No data")?.login_session_consume { + spinner.finish_and_clear(); + configs.root_config.user.token = Some(token); + configs.write()?; + + let client = GQLClient::new_authorized(&configs)?; + let vars = queries::user_meta::Variables {}; + + let res = post_graphql::(&client, configs.get_backboard(), vars) + .await?; + let me = res.data.context("No data")?.me; + + spinner.finish_and_clear(); + println!( + "Logged in as {} ({})", + me.name.context("No name")?.bold(), + me.email + ); + break; + } + } + Ok(()) +} diff --git a/src/commands/logout.rs b/src/commands/logout.rs new file mode 100644 index 0000000..ffb8cff --- /dev/null +++ b/src/commands/logout.rs @@ -0,0 +1,13 @@ +use super::*; + +/// Logout of your Railway account +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + let mut configs = Configs::new()?; + configs.reset()?; + configs.write()?; + println!("Logged out successfully"); + Ok(()) +} diff --git a/src/commands/logs.rs b/src/commands/logs.rs new file mode 100644 index 0000000..3c74a15 --- /dev/null +++ b/src/commands/logs.rs @@ -0,0 +1,83 @@ +use futures::StreamExt; + +use crate::subscription::subscribe_graphql; + +use super::*; + +/// View the most-recent deploy's logs +#[derive(Parser)] +pub struct Args { + /// Show deployment logs + #[clap(short, long, group = "log_type")] + deployment: bool, + + /// Show build logs + #[clap(short, long, group = "log_type")] + build: bool, +} + +pub async fn command(args: Args, json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::deployments::Variables { + project_id: linked_project.project.clone(), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + + let mut deployments: Vec<_> = body + .project + .deployments + .edges + .into_iter() + .map(|deployment| deployment.node) + .collect(); + deployments.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + let latest_deployment = deployments.first().context("No deployments found")?; + + if args.build && !args.deployment { + let vars = subscriptions::build_logs::Variables { + deployment_id: latest_deployment.id.clone(), + filter: Some(String::new()), + limit: Some(500), + }; + + let (_client, mut log_stream) = subscribe_graphql::(vars).await?; + while let Some(Ok(log)) = log_stream.next().await { + let log = log.data.context("Failed to retrieve log")?; + for line in log.build_logs { + if json { + println!("{}", serde_json::to_string(&line)?); + } else { + println!("{}", line.message); + } + } + } + } else { + let vars = subscriptions::deployment_logs::Variables { + deployment_id: latest_deployment.id.clone(), + filter: Some(String::new()), + limit: Some(500), + }; + + let (_client, mut log_stream) = + subscribe_graphql::(vars).await?; + while let Some(Ok(log)) = log_stream.next().await { + let log = log.data.context("Failed to retrieve log")?; + for line in log.deployment_logs { + if json { + println!("{}", serde_json::to_string(&line)?); + } else { + println!("{}", line.message); + } + } + } + } + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..fcb2b03 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,27 @@ +pub(super) use crate::{client::*, config::*, gql::*}; +pub(super) use anyhow::{Context, Result}; +pub(super) use clap::Parser; +pub(super) use colored::Colorize; + +pub mod add; +pub mod completion; +pub mod delete; +pub mod docs; +pub mod domain; +pub mod environment; +pub mod init; +pub mod link; +pub mod list; +pub mod login; +pub mod logout; +pub mod logs; +pub mod open; +pub mod run; +pub mod service; +pub mod shell; +pub mod starship; +pub mod status; +pub mod unlink; +pub mod up; +pub mod variables; +pub mod whoami; diff --git a/src/commands/open.rs b/src/commands/open.rs new file mode 100644 index 0000000..66cf7fc --- /dev/null +++ b/src/commands/open.rs @@ -0,0 +1,25 @@ +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::consts::NON_INTERACTIVE_FAILURE; + +use super::*; + +/// Open your project dashboard +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + if !std::io::stdout().is_terminal() { + bail!(NON_INTERACTIVE_FAILURE); + } + + let configs = Configs::new()?; + let hostname = configs.get_host(); + let linked_project = configs.get_linked_project().await?; + ::open::that(format!( + "https://{hostname}/project/{}", + linked_project.project + ))?; + Ok(()) +} diff --git a/src/commands/run.rs b/src/commands/run.rs new file mode 100644 index 0000000..08ef0b5 --- /dev/null +++ b/src/commands/run.rs @@ -0,0 +1,137 @@ +use std::collections::BTreeMap; + +use anyhow::bail; + +use super::*; + +/// Run a local command using variables from the active environment +#[derive(Debug, Parser)] +pub struct Args { + /// Service to pull variables from (defaults to linked service) + #[clap(short, long)] + service: Option, + + /// Environment to pull variables from (defaults to linked environment) + #[clap(short, long)] + environment: Option, + + /// Args to pass to the command + #[clap(trailing_var_arg = true)] + args: Vec, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + let mut all_variables = BTreeMap::::new(); + + let plugins: Vec<_> = body + .project + .plugins + .edges + .iter() + .map(|plugin| &plugin.node) + .collect(); + let environment_id = args + .environment + .clone() + .unwrap_or(linked_project.environment.clone()); + for plugin in plugins { + let vars = queries::variables::Variables { + environment_id: environment_id.clone(), + project_id: linked_project.project.clone(), + service_id: None, + plugin_id: Some(plugin.id.clone()), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + if body.variables.is_empty() { + continue; + } + + all_variables.append(&mut body.variables); + } + if let Some(service) = args.service { + let service_id = body + .project + .services + .edges + .iter() + .find(|s| s.node.name == service || s.node.id == service) + .context("Service not found")?; + + let vars = queries::variables::Variables { + environment_id: environment_id.clone(), + project_id: linked_project.project.clone(), + service_id: Some(service_id.node.id.clone()), + plugin_id: None, + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + all_variables.append(&mut body.variables); + } else if linked_project.service.is_some() { + let vars = queries::variables::Variables { + environment_id: environment_id.clone(), + project_id: linked_project.project.clone(), + service_id: linked_project.service.clone(), + plugin_id: None, + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + all_variables.append(&mut body.variables); + } else { + let services: Vec<_> = body.project.services.edges.iter().collect(); + if services.len() > 1 { + bail!( + "Multiple services found, please link one using {}", + "railway service".bold().dimmed() + ); + } + let service_id = services.first().context("No services found")?; + + let vars = queries::variables::Variables { + environment_id: environment_id.clone(), + project_id: linked_project.project.clone(), + service_id: Some(service_id.node.id.clone()), + plugin_id: None, + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + all_variables.append(&mut body.variables); + } + + tokio::process::Command::new(args.args.first().context("No command provided")?) + .args(args.args[1..].iter()) + .envs(all_variables) + .spawn() + .context("Failed to spawn command")? + .wait() + .await + .context("Failed to wait for command")?; + Ok(()) +} diff --git a/src/commands/service.rs b/src/commands/service.rs new file mode 100644 index 0000000..3b44639 --- /dev/null +++ b/src/commands/service.rs @@ -0,0 +1,80 @@ +use std::fmt::Display; + +use anyhow::bail; + +use crate::consts::SERVICE_NOT_FOUND; + +use super::{queries::project::ProjectProjectServicesEdgesNode, *}; + +/// Link a service to the current project +#[derive(Parser)] +pub struct Args { + /// The service to link + service: Option, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let mut configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + + let services: Vec<_> = body + .project + .services + .edges + .iter() + .map(|env| Service(&env.node)) + .collect(); + + if let Some(service) = args.service { + let service = services + .iter() + .find(|env| env.0.id == service || env.0.name == service) + .context("Service not found")?; + let vars = queries::project::Variables { + id: service.0.id.clone(), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + let body = res.data.context(SERVICE_NOT_FOUND)?; + + configs.link_project( + body.project.id.clone(), + Some(body.project.name), + linked_project.environment.clone(), + linked_project.environment_name.clone(), + )?; + configs.write()?; + return Ok(()); + } + + if services.is_empty() { + bail!("No services found"); + } + + let service = inquire::Select::new("Select a service", services) + .with_render_config(Configs::get_render_config()) + .prompt()?; + + configs.link_service(service.0.id.clone())?; + configs.write()?; + Ok(()) +} + +#[derive(Debug, Clone)] +struct Service<'a>(&'a ProjectProjectServicesEdgesNode); + +impl<'a> Display for Service<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.name) + } +} diff --git a/src/commands/shell.rs b/src/commands/shell.rs new file mode 100644 index 0000000..43b2352 --- /dev/null +++ b/src/commands/shell.rs @@ -0,0 +1,109 @@ +use std::collections::BTreeMap; + +use super::*; + +/// Open a subshell with Railway variables available +#[derive(Parser)] +pub struct Args { + /// Service to pull variables from (defaults to linked service) + #[clap(short, long)] + service: Option, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + let mut all_variables = BTreeMap::::new(); + all_variables.insert("IN_RAILWAY_SHELL".to_owned(), "true".to_owned()); + + let plugins: Vec<_> = body + .project + .plugins + .edges + .iter() + .map(|plugin| &plugin.node) + .collect(); + + for plugin in plugins { + let vars = queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: None, + plugin_id: Some(plugin.id.clone()), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + if body.variables.is_empty() { + continue; + } + + all_variables.append(&mut body.variables); + } + + if let Some(service) = args.service { + let service_id = body + .project + .services + .edges + .iter() + .find(|s| s.node.name == service || s.node.id == service) + .context("Service not found")?; + + let vars = queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: Some(service_id.node.id.clone()), + plugin_id: None, + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + all_variables.append(&mut body.variables); + } else if linked_project.service.is_some() { + let vars = queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: linked_project.service.clone(), + plugin_id: None, + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let mut body = res.data.context("Failed to retrieve response body")?; + + all_variables.append(&mut body.variables); + } else { + eprintln!("No service linked, skipping service variables"); + } + + let shell = std::env::var("SHELL").unwrap_or(match std::env::consts::OS { + "windows" => "cmd".to_string(), + _ => "sh".to_string(), + }); + + tokio::process::Command::new(shell) + .envs(all_variables) + .spawn() + .context("Failed to spawn command")? + .wait() + .await + .context("Failed to wait for command")?; + Ok(()) +} diff --git a/src/commands/starship.rs b/src/commands/starship.rs new file mode 100644 index 0000000..4f6873d --- /dev/null +++ b/src/commands/starship.rs @@ -0,0 +1,13 @@ +use super::*; + +/// Starship Metadata +#[derive(Parser)] +#[clap(hide = true)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + let linked_project = configs.get_linked_project().await?; + println!("{}", serde_json::to_string(&linked_project)?); + Ok(()) +} diff --git a/src/commands/status.rs b/src/commands/status.rs new file mode 100644 index 0000000..1ac471c --- /dev/null +++ b/src/commands/status.rs @@ -0,0 +1,56 @@ +use super::*; + +/// Show information about the current project +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + if !json { + println!("Project: {}", body.project.name.purple().bold()); + println!( + "Environment: {}", + body.project + .environments + .edges + .iter() + .map(|env| &env.node) + .find(|env| env.id == linked_project.environment) + .context("Environment not found!")? + .name + .blue() + .bold() + ); + if !body.project.plugins.edges.is_empty() { + println!("Plugins:"); + for plugin in body.project.plugins.edges.iter().map(|plugin| &plugin.node) { + println!("{}", format!("{:?}", plugin.name).dimmed().bold()); + } + } + if !body.project.services.edges.is_empty() { + println!("Services:"); + for service in body + .project + .services + .edges + .iter() + .map(|service| &service.node) + { + println!("{}", service.name.dimmed().bold()); + } + } + } else { + println!("{}", serde_json::to_string_pretty(&body.project)?); + } + Ok(()) +} diff --git a/src/commands/unlink.rs b/src/commands/unlink.rs new file mode 100644 index 0000000..c781466 --- /dev/null +++ b/src/commands/unlink.rs @@ -0,0 +1,86 @@ +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::consts::ABORTED_BY_USER; + +use super::*; + +/// Disassociate project from current directory +#[derive(Parser)] +pub struct Args { + /// Unlink a service + #[clap(short, long)] + service: bool, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let mut configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + let linked_service = body + .project + .services + .edges + .iter() + .find(|service| Some(service.node.id.clone()) == linked_project.service); + + if args.service { + let Some(service) = linked_service else { + bail!("No linked service"); + }; + println!( + "Linked to {} on {}", + service.node.name.bold(), + body.project.name.bold() + ); + let confirmed = !if std::io::stdout().is_terminal() { + inquire::Confirm::new("Are you sure you want to unlink this service?") + .with_render_config(Configs::get_render_config()) + .with_default(true) + .prompt()? + } else { + true + }; + + if !confirmed { + bail!(ABORTED_BY_USER); + } + configs.unlink_service()?; + configs.write()?; + return Ok(()); + } + + if let Some(service) = linked_service { + println!( + "Linked to {} on {}", + service.node.name.bold(), + body.project.name.bold() + ); + } else { + println!("Linked to {}", body.project.name.bold()); + } + + let confirmed = !if std::io::stdout().is_terminal() { + inquire::Confirm::new("Are you sure you want to unlink this project?") + .with_render_config(Configs::get_render_config()) + .with_default(true) + .prompt()? + } else { + true + }; + + if !confirmed { + bail!(ABORTED_BY_USER); + } + configs.unlink_project()?; + configs.write()?; + Ok(()) +} diff --git a/src/commands/up.rs b/src/commands/up.rs new file mode 100644 index 0000000..1449ac8 --- /dev/null +++ b/src/commands/up.rs @@ -0,0 +1,164 @@ +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, + time::Duration, +}; + +use futures::StreamExt; +use gzp::{deflate::Gzip, ZBuilder}; +use ignore::WalkBuilder; +use indicatif::{ProgressBar, ProgressFinish, ProgressIterator, ProgressStyle}; +use is_terminal::IsTerminal; +use serde::{Deserialize, Serialize}; +use synchronized_writer::SynchronizedWriter; +use tar::Builder; + +use crate::{consts::TICK_STRING, subscription::subscribe_graphql}; + +use super::*; + +/// Upload and deploy project from the current directory +#[derive(Parser)] +pub struct Args { + path: Option, + + #[clap(short, long)] + /// Don't attach to the log stream + detach: bool, +} + +pub async fn command(args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + let hostname = configs.get_host(); + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + let spinner = if !std::io::stdout().is_terminal() { + let spinner = ProgressBar::new_spinner() + .with_style( + ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg:.cyan.bold}")?, + ) + .with_message("Indexing".to_string()); + spinner.enable_steady_tick(Duration::from_millis(100)); + Some(spinner) + } else { + println!("Indexing..."); + None + }; + let bytes = Vec::::new(); + let arc = Arc::new(Mutex::new(bytes)); + let mut parz = ZBuilder::::new() + .num_threads(num_cpus::get()) + .from_writer(SynchronizedWriter::new(arc.clone())); + { + let mut archive = Builder::new(&mut parz); + let mut builder = WalkBuilder::new(args.path.unwrap_or_else(|| ".".into())); + builder.add_custom_ignore_filename(".railwayignore"); + let walker = builder.follow_links(true).hidden(false); + let walked = walker.build().collect::>(); + if let Some(spinner) = spinner { + spinner.finish_with_message("Indexed"); + } + if !std::io::stdout().is_terminal() { + let pg = ProgressBar::new(walked.len() as u64) + .with_style( + ProgressStyle::default_bar() + .template("{spinner:.green} {msg:.cyan.bold} [{bar:20}] {percent}% ")? + .progress_chars("=> ") + .tick_chars(TICK_STRING), + ) + .with_message("Compressing") + .with_finish(ProgressFinish::WithMessage("Compressed".into())); + pg.enable_steady_tick(Duration::from_millis(100)); + + for entry in walked.into_iter().progress_with(pg) { + archive.append_path(entry?.path())?; + } + } else { + for entry in walked.into_iter() { + archive.append_path(entry?.path())?; + } + } + } + parz.finish()?; + + let builder = client.post(format!( + "https://backboard.{hostname}/project/{}/environment/{}/up", + linked_project.project, linked_project.environment + )); + let spinner = if !std::io::stdout().is_terminal() { + let spinner = ProgressBar::new_spinner() + .with_style( + ProgressStyle::default_spinner() + .tick_chars(TICK_STRING) + .template("{spinner:.green} {msg:.cyan.bold}")?, + ) + .with_message("Uploading"); + spinner.enable_steady_tick(Duration::from_millis(100)); + Some(spinner) + } else { + println!("Uploading..."); + None + }; + + let body = arc.lock().unwrap().clone(); + let res = builder + .header("Content-Type", "multipart/form-data") + .body(body) + .send() + .await? + .error_for_status()?; + + let body = res.json::().await?; + if let Some(spinner) = spinner { + spinner.finish_with_message("Uploaded"); + } + println!(" {}: {}", "Build Logs".green().bold(), body.logs_url); + if args.detach { + return Ok(()); + } + + let vars = queries::deployments::Variables { + project_id: linked_project.project.clone(), + }; + + let res = + post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + + let mut deployments: Vec<_> = body + .project + .deployments + .edges + .into_iter() + .map(|deployment| deployment.node) + .collect(); + deployments.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + let latest_deployment = deployments.first().context("No deployments found")?; + if !std::io::stdout().is_terminal() { + let vars = subscriptions::build_logs::Variables { + deployment_id: latest_deployment.id.clone(), + filter: Some(String::new()), + limit: Some(500), + }; + + let (_client, mut log_stream) = subscribe_graphql::(vars).await?; + while let Some(Ok(log)) = log_stream.next().await { + let log = log.data.context("Failed to retrieve log")?; + for line in log.build_logs { + println!("{}", line.message); + } + } + } + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpResponse { + pub url: String, + pub logs_url: String, + pub deployment_domain: String, +} diff --git a/src/commands/variables.rs b/src/commands/variables.rs new file mode 100644 index 0000000..180cb07 --- /dev/null +++ b/src/commands/variables.rs @@ -0,0 +1,167 @@ +use std::fmt::Display; + +use anyhow::bail; +use is_terminal::IsTerminal; + +use crate::{consts::NO_SERVICE_LINKED, table::Table}; + +use super::{ + queries::project::{PluginType, ProjectProjectPluginsEdgesNode}, + *, +}; + +/// Show variables for active environment +#[derive(Parser)] +pub struct Args { + /// Show variables for a plugin + #[clap(short, long)] + plugin: bool, + + /// Show variables for a specific service + #[clap(short, long)] + service: Option, + + /// Show variables in KV format + #[clap(short, long)] + kv: bool, +} + +pub async fn command(args: Args, json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + let vars = queries::project::Variables { + id: linked_project.project.to_owned(), + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + let plugins: Vec<_> = body + .project + .plugins + .edges + .iter() + .map(|plugin| Plugin(&plugin.node)) + .collect(); + + let (vars, name) = if args.plugin { + if plugins.is_empty() { + bail!("No plugins found"); + } + let plugin = prompt_plugin(plugins)?; + ( + queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: None, + plugin_id: Some(plugin.0.id.clone()), + }, + format!("{plugin}"), + ) + } else if let Some(ref service) = args.service { + let service_name = body + .project + .services + .edges + .iter() + .find(|edge| edge.node.id == *service || edge.node.name == *service) + .context("Service not found")?; + ( + queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: Some(service_name.node.id.clone()), + plugin_id: None, + }, + service_name.node.name.clone(), + ) + } else if let Some(ref service) = linked_project.service { + let service_name = body + .project + .services + .edges + .iter() + .find(|edge| edge.node.id == *service) + .context("Service not found")?; + ( + queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: Some(service.clone()), + plugin_id: None, + }, + service_name.node.name.clone(), + ) + } else { + if plugins.is_empty() { + bail!(NO_SERVICE_LINKED); + } + let plugin = prompt_plugin(plugins)?; + ( + queries::variables::Variables { + environment_id: linked_project.environment.clone(), + project_id: linked_project.project.clone(), + service_id: None, + plugin_id: Some(plugin.0.id.clone()), + }, + format!("{plugin}"), + ) + }; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + + let body = res.data.context("Failed to retrieve response body")?; + + if body.variables.is_empty() { + eprintln!("No variables found"); + return Ok(()); + } + + if args.kv { + for (key, value) in body.variables { + println!("{}={}", key, value); + } + return Ok(()); + } + + if json { + println!("{}", serde_json::to_string_pretty(&body.variables)?); + return Ok(()); + } + + let table = Table::new(name, body.variables); + table.print()?; + + Ok(()) +} + +fn prompt_plugin(plugins: Vec) -> Result { + if !std::io::stdout().is_terminal() { + bail!("Plugin must be provided when not running in a terminal") + } + let plugin = inquire::Select::new("Select a plugin", plugins) + .with_render_config(Configs::get_render_config()) + .prompt()?; + + Ok(plugin) +} + +struct Plugin<'a>(&'a ProjectProjectPluginsEdgesNode); + +impl<'a> Display for Plugin<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match &self.0.name { + PluginType::mongodb => "MongoDB", + PluginType::mysql => "MySQL", + PluginType::postgresql => "PostgreSQL", + PluginType::redis => "Redis", + PluginType::Other(plugin) => plugin, + } + ) + } +} diff --git a/src/commands/whoami.rs b/src/commands/whoami.rs new file mode 100644 index 0000000..c94fc66 --- /dev/null +++ b/src/commands/whoami.rs @@ -0,0 +1,22 @@ +use super::*; + +/// Get the current logged in user +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, _json: bool) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let vars = queries::user_meta::Variables {}; + + let res = post_graphql::(&client, configs.get_backboard(), vars).await?; + let me = res.data.context("No data")?.me; + + println!( + "Logged in as {} ({})", + me.name.context("No name")?.bold(), + me.email + ); + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..195657d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,283 @@ +use std::{ + collections::BTreeMap, + fs::{create_dir_all, File}, + io::{Read, Write}, + path::PathBuf, +}; + +use anyhow::{Context, Result}; +use colored::Colorize; +use inquire::ui::{Attributes, RenderConfig, StyleSheet, Styled}; +use serde::{Deserialize, Serialize}; + +use crate::{ + client::{post_graphql, GQLClient}, + commands::queries, +}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct RailwayProject { + pub project_path: String, + pub name: Option, + pub project: String, + pub environment: String, + pub environment_name: Option, + pub service: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct RailwayUser { + pub token: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct RailwayConfig { + pub projects: BTreeMap, + pub user: RailwayUser, +} + +#[derive(Debug)] +#[serde_with::skip_serializing_none] +pub struct Configs { + pub root_config: RailwayConfig, + root_config_path: PathBuf, +} + +pub enum Environment { + Production, + Staging, + Dev, +} + +impl Configs { + pub fn new() -> Result { + let environment = Self::get_environment_id(); + let root_config_partial_path = match environment { + Environment::Production => ".railway/config.json", + Environment::Staging => ".railway/config-staging.json", + Environment::Dev => ".railway/config-dev.json", + }; + + let home_dir = dirs::home_dir().context("Unable to get home directory")?; + let root_config_path = std::path::Path::new(&home_dir).join(root_config_partial_path); + + if let Ok(mut file) = File::open(&root_config_path) { + let mut serialized_config = vec![]; + file.read_to_end(&mut serialized_config)?; + + let root_config: RailwayConfig = serde_json::from_slice(&serialized_config) + .unwrap_or_else(|_| { + eprintln!("{}", "Unable to parse config file, regenerating".yellow()); + RailwayConfig { + projects: BTreeMap::new(), + user: RailwayUser { token: None }, + } + }); + + let config = Self { + root_config, + root_config_path, + }; + + config.write()?; + + return Ok(config); + } + + Ok(Self { + root_config_path, + root_config: RailwayConfig { + projects: BTreeMap::new(), + user: RailwayUser { token: None }, + }, + }) + } + + pub fn reset(&mut self) -> Result<()> { + self.root_config = RailwayConfig { + projects: BTreeMap::new(), + user: RailwayUser { token: None }, + }; + Ok(()) + } + + pub fn get_railway_token() -> Option { + std::env::var("RAILWAY_TOKEN").ok() + } + + pub fn get_railway_api_token() -> Option { + std::env::var("RAILWAY_API_TOKEN").ok() + } + + pub fn get_environment_id() -> Environment { + match std::env::var("RAILWAY_ENV") + .map(|env| env.to_lowercase()) + .as_deref() + { + Ok("production") => Environment::Production, + Ok("staging") => Environment::Staging, + Ok("dev") => Environment::Dev, + Ok("develop") => Environment::Dev, + _ => Environment::Production, + } + } + + pub fn get_host(&self) -> &'static str { + match Self::get_environment_id() { + Environment::Production => "railway.app", + Environment::Staging => "railway-staging.app", + Environment::Dev => "railway-develop.app", + } + } + + pub fn get_backboard(&self) -> String { + format!("https://backboard.{}/graphql/v2", self.get_host()) + } + + pub fn get_current_directory(&self) -> Result { + let current_dir = std::env::current_dir()?; + let path = current_dir + .to_str() + .context("Unable to get current working directory")?; + Ok(path.to_owned()) + } + + pub fn get_closest_linked_project_directory(&self) -> Result { + let current_dir = std::env::current_dir()?; + let path = current_dir + .to_str() + .context("Unable to get current working directory")?; + let mut current_path = PathBuf::from(path); + loop { + let path = current_path + .to_str() + .context("Unable to get current working directory")? + .to_owned(); + let config = self.root_config.projects.get(&path); + if config.is_some() { + return Ok(path); + } + if !current_path.pop() { + break; + } + } + Err(anyhow::anyhow!("No linked project found")) + } + + pub async fn get_linked_project(&self) -> Result { + if Self::get_railway_token().is_some() { + let vars = queries::project_token::Variables {}; + let client = GQLClient::new_authorized(self)?; + + let res = post_graphql::(&client, self.get_backboard(), vars) + .await?; + + let data = res.data.context("Invalid project token!")?; + + let project = RailwayProject { + project_path: self.get_current_directory()?, + name: Some(data.project_token.project.name), + project: data.project_token.project.id, + environment: data.project_token.environment.id, + environment_name: Some(data.project_token.environment.name), + service: None, + }; + return Ok(project); + } + let path = self.get_closest_linked_project_directory()?; + let project = self + .root_config + .projects + .get(&path) + .context("Project not found! Run `railway link` to link to a project")?; + Ok(project.clone()) + } + + pub fn get_linked_project_mut(&mut self) -> Result<&mut RailwayProject> { + let path = self.get_closest_linked_project_directory()?; + let project = self + .root_config + .projects + .get_mut(&path) + .context("Project not found! Run `railway link` to link to a project")?; + Ok(project) + } + + pub fn link_project( + &mut self, + project_id: String, + name: Option, + environment_id: String, + environment_name: Option, + ) -> Result<()> { + let path = self.get_current_directory()?; + let project = RailwayProject { + project_path: path.clone(), + name, + project: project_id, + environment: environment_id, + environment_name, + service: None, + }; + self.root_config.projects.insert(path, project); + Ok(()) + } + + pub fn link_service(&mut self, service_id: String) -> Result<()> { + let linked_project = self.get_linked_project_mut()?; + linked_project.service = Some(service_id); + Ok(()) + } + + pub fn unlink_project(&mut self) -> Result { + let path = self.get_closest_linked_project_directory()?; + let project = self + .root_config + .projects + .remove(&path) + .context("Project not found! Run `railway link` to link to a project")?; + Ok(project) + } + + pub fn unlink_service(&mut self) -> Result<()> { + let linked_project = self.get_linked_project_mut()?; + linked_project.service = None; + Ok(()) + } + + pub fn get_render_config() -> RenderConfig { + RenderConfig::default_colored() + .with_help_message( + StyleSheet::new() + .with_fg(inquire::ui::Color::LightMagenta) + .with_attr(Attributes::BOLD), + ) + .with_answer( + StyleSheet::new() + .with_fg(inquire::ui::Color::LightCyan) + .with_attr(Attributes::BOLD), + ) + .with_prompt_prefix( + Styled::new("?").with_style_sheet( + StyleSheet::new() + .with_fg(inquire::ui::Color::LightCyan) + .with_attr(Attributes::BOLD), + ), + ) + } + + pub fn write(&self) -> Result<()> { + create_dir_all(self.root_config_path.parent().unwrap())?; + let mut file = File::create(&self.root_config_path)?; + let serialized_config = serde_json::to_vec_pretty(&self.root_config)?; + file.write_all(serialized_config.as_slice())?; + file.sync_all()?; + Ok(()) + } +} diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..e13643b --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,14 @@ +pub const fn get_user_agent() -> &'static str { + concat!("CLI ", env!("CARGO_PKG_VERSION")) +} + +pub const TICK_STRING: &str = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ "; + +pub const PLUGINS: &[&str] = &["PostgreSQL", "MySQL", "Redis", "MongoDB"]; + +pub const NO_SERVICE_LINKED: &str = + "No service linked and no plugins found\nRun `railway service` to link a service"; +pub const ABORTED_BY_USER: &str = "Aborted by user"; +pub const PROJECT_NOT_FOUND: &str = "Project not found!"; +pub const SERVICE_NOT_FOUND: &str = "Service not found!"; +pub const NON_INTERACTIVE_FAILURE: &str = "This command is only available in interactive mode"; diff --git a/src/gql/mod.rs b/src/gql/mod.rs new file mode 100644 index 0000000..9f27eee --- /dev/null +++ b/src/gql/mod.rs @@ -0,0 +1,3 @@ +pub mod mutations; +pub mod queries; +pub mod subscriptions; diff --git a/src/gql/mutations/mod.rs b/src/gql/mutations/mod.rs new file mode 100644 index 0000000..1bf4a77 --- /dev/null +++ b/src/gql/mutations/mod.rs @@ -0,0 +1,57 @@ +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/PluginCreate.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct PluginCreate; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/PluginDelete.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct PluginDelete; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/ValidateTwoFactor.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct ValidateTwoFactor; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/ProjectCreate.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct ProjectCreate; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/LoginSessionCreate.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct LoginSessionCreate; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/LoginSessionConsume.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct LoginSessionConsume; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/mutations/strings/ServiceDomainCreate.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct ServiceDomainCreate; diff --git a/src/gql/mutations/strings/LoginSessionConsume.graphql b/src/gql/mutations/strings/LoginSessionConsume.graphql new file mode 100644 index 0000000..a4dc2d6 --- /dev/null +++ b/src/gql/mutations/strings/LoginSessionConsume.graphql @@ -0,0 +1,3 @@ +mutation LoginSessionConsume($code: String!) { + loginSessionConsume(code: $code) +} diff --git a/src/gql/mutations/strings/LoginSessionCreate.graphql b/src/gql/mutations/strings/LoginSessionCreate.graphql new file mode 100644 index 0000000..5bbdc53 --- /dev/null +++ b/src/gql/mutations/strings/LoginSessionCreate.graphql @@ -0,0 +1,3 @@ +mutation LoginSessionCreate { + loginSessionCreate +} diff --git a/src/gql/mutations/strings/PluginCreate.graphql b/src/gql/mutations/strings/PluginCreate.graphql new file mode 100644 index 0000000..ab363b5 --- /dev/null +++ b/src/gql/mutations/strings/PluginCreate.graphql @@ -0,0 +1,5 @@ +mutation PluginCreate($name: String!, $projectId: String!) { + pluginCreate(input: { name: $name, projectId: $projectId }) { + id + } +} diff --git a/src/gql/mutations/strings/PluginDelete.graphql b/src/gql/mutations/strings/PluginDelete.graphql new file mode 100644 index 0000000..6ab8877 --- /dev/null +++ b/src/gql/mutations/strings/PluginDelete.graphql @@ -0,0 +1,3 @@ +mutation PluginDelete($id: String!) { + pluginDelete(id: $id) +} diff --git a/src/gql/mutations/strings/ProjectCreate.graphql b/src/gql/mutations/strings/ProjectCreate.graphql new file mode 100644 index 0000000..59e59f5 --- /dev/null +++ b/src/gql/mutations/strings/ProjectCreate.graphql @@ -0,0 +1,16 @@ +mutation ProjectCreate($name: String, $description: String, $teamId: String) { + projectCreate( + input: { name: $name, description: $description, teamId: $teamId } + ) { + name + id + environments { + edges { + node { + id + name + } + } + } + } +} diff --git a/src/gql/mutations/strings/ServiceDomainCreate.graphql b/src/gql/mutations/strings/ServiceDomainCreate.graphql new file mode 100644 index 0000000..b1ce968 --- /dev/null +++ b/src/gql/mutations/strings/ServiceDomainCreate.graphql @@ -0,0 +1,8 @@ +mutation ServiceDomainCreate($environmentId: String!, $serviceId: String!) { + serviceDomainCreate( + input: { environmentId: $environmentId, serviceId: $serviceId } + ) { + id + domain + } +} diff --git a/src/gql/mutations/strings/ValidateTwoFactor.graphql b/src/gql/mutations/strings/ValidateTwoFactor.graphql new file mode 100644 index 0000000..d670d91 --- /dev/null +++ b/src/gql/mutations/strings/ValidateTwoFactor.graphql @@ -0,0 +1,3 @@ +mutation ValidateTwoFactor($token: String!) { + twoFactorInfoValidate(input: { token: $token }) +} diff --git a/src/gql/queries/mod.rs b/src/gql/queries/mod.rs new file mode 100644 index 0000000..d5f11d0 --- /dev/null +++ b/src/gql/queries/mod.rs @@ -0,0 +1,92 @@ +use graphql_client::GraphQLQuery; + +type DateTime = chrono::DateTime; +type ServiceVariables = std::collections::BTreeMap; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/Project.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct Project; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/Projects.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct Projects; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/UserMeta.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct UserMeta; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/ProjectPlugins.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct ProjectPlugins; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/TwoFactorInfo.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct TwoFactorInfo; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/UserProjects.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct UserProjects; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/Variables.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct Variables; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/Deployments.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct Deployments; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/BuildLogs.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct BuildLogs; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/Domains.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct Domains; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/queries/strings/ProjectToken.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct ProjectToken; diff --git a/src/gql/queries/strings/BuildLogs.graphql b/src/gql/queries/strings/BuildLogs.graphql new file mode 100644 index 0000000..eec2c1d --- /dev/null +++ b/src/gql/queries/strings/BuildLogs.graphql @@ -0,0 +1,6 @@ +query BuildLogs($deploymentId: String!, $startDate: DateTime) { + buildLogs(deploymentId: $deploymentId, startDate: $startDate) { + message + timestamp + } +} diff --git a/src/gql/queries/strings/Deployments.graphql b/src/gql/queries/strings/Deployments.graphql new file mode 100644 index 0000000..2ccfed9 --- /dev/null +++ b/src/gql/queries/strings/Deployments.graphql @@ -0,0 +1,12 @@ +query Deployments($projectId: String!) { + project(id: $projectId) { + deployments { + edges { + node { + id + createdAt + } + } + } + } +} diff --git a/src/gql/queries/strings/Domains.graphql b/src/gql/queries/strings/Domains.graphql new file mode 100644 index 0000000..d4f0cb3 --- /dev/null +++ b/src/gql/queries/strings/Domains.graphql @@ -0,0 +1,18 @@ +query Domains( + $environmentId: String! + $projectId: String! + $serviceId: String! +) { + domains( + environmentId: $environmentId + projectId: $projectId + serviceId: $serviceId + ) { + serviceDomains { + id + } + customDomains { + id + } + } +} diff --git a/src/gql/queries/strings/Project.graphql b/src/gql/queries/strings/Project.graphql new file mode 100644 index 0000000..3e43941 --- /dev/null +++ b/src/gql/queries/strings/Project.graphql @@ -0,0 +1,30 @@ +query Project($id: String!) { + project(id: $id) { + id + name + plugins { + edges { + node { + id + name + } + } + } + environments { + edges { + node { + id + name + } + } + } + services { + edges { + node { + id + name + } + } + } + } +} diff --git a/src/gql/queries/strings/ProjectPlugins.graphql b/src/gql/queries/strings/ProjectPlugins.graphql new file mode 100644 index 0000000..055285b --- /dev/null +++ b/src/gql/queries/strings/ProjectPlugins.graphql @@ -0,0 +1,14 @@ +query ProjectPlugins($id: String!) { + project(id: $id) { + id + name + plugins { + edges { + node { + id + name + } + } + } + } +} diff --git a/src/gql/queries/strings/ProjectToken.graphql b/src/gql/queries/strings/ProjectToken.graphql new file mode 100644 index 0000000..0beafe3 --- /dev/null +++ b/src/gql/queries/strings/ProjectToken.graphql @@ -0,0 +1,13 @@ +query ProjectToken { + projectToken { + id + project { + id + name + } + environment { + id + name + } + } +} diff --git a/src/gql/queries/strings/Projects.graphql b/src/gql/queries/strings/Projects.graphql new file mode 100644 index 0000000..8544bbc --- /dev/null +++ b/src/gql/queries/strings/Projects.graphql @@ -0,0 +1,23 @@ +query Projects($teamId: String) { + projects(teamId: $teamId) { + edges { + node { + id + name + updatedAt + team { + id + name + } + environments { + edges { + node { + id + name + } + } + } + } + } + } +} diff --git a/src/gql/queries/strings/TwoFactorInfo.graphql b/src/gql/queries/strings/TwoFactorInfo.graphql new file mode 100644 index 0000000..0f0b432 --- /dev/null +++ b/src/gql/queries/strings/TwoFactorInfo.graphql @@ -0,0 +1,6 @@ +query TwoFactorInfo { + twoFactorInfo { + isVerified + hasRecoveryCodes + } +} diff --git a/src/gql/queries/strings/UserMeta.graphql b/src/gql/queries/strings/UserMeta.graphql new file mode 100644 index 0000000..324c310 --- /dev/null +++ b/src/gql/queries/strings/UserMeta.graphql @@ -0,0 +1,6 @@ +query UserMeta { + me { + name + email + } +} diff --git a/src/gql/queries/strings/UserProjects.graphql b/src/gql/queries/strings/UserProjects.graphql new file mode 100644 index 0000000..260e0e9 --- /dev/null +++ b/src/gql/queries/strings/UserProjects.graphql @@ -0,0 +1,34 @@ +query UserProjects { + me { + projects { + edges { + node { + id + name + createdAt + updatedAt + team { + id + name + } + environments { + edges { + node { + id + name + } + } + } + } + } + } + teams { + edges { + node { + id + name + } + } + } + } +} diff --git a/src/gql/queries/strings/Variables.graphql b/src/gql/queries/strings/Variables.graphql new file mode 100644 index 0000000..19e989f --- /dev/null +++ b/src/gql/queries/strings/Variables.graphql @@ -0,0 +1,13 @@ +query Variables( + $projectId: String! + $environmentId: String! + $serviceId: String + $pluginId: String +) { + variables( + projectId: $projectId + environmentId: $environmentId + serviceId: $serviceId + pluginId: $pluginId + ) +} diff --git a/src/gql/schema.graphql b/src/gql/schema.graphql new file mode 100644 index 0000000..2341cdc --- /dev/null +++ b/src/gql/schema.graphql @@ -0,0 +1,2117 @@ +type AccessRule { + disallowed: String +} + +""" +The aggregated usage of a single measurement. +""" +type AggregatedUsage { + """ + The measurement that was aggregated. + """ + measurement: MetricMeasurement! + + """ + The tags that were used to group the metric. Only the tags that were used in the `groupBy` will be present. + """ + tags: MetricTags! + + """ + The aggregated value. + """ + value: Float! +} + +type AllDomains { + customDomains: [CustomDomain!]! + serviceDomains: [ServiceDomain!]! +} + +type ApiToken implements Node { + displayToken: String! + id: ID! + name: String! + teamId: String +} + +input ApiTokenCreateInput { + name: String! + teamId: String +} + +type BanReasonHistory implements Node { + actor: User! + banReason: String + createdAt: DateTime! + id: ID! +} + +input BaseEnvironmentOverrideInput { + baseEnvironmentOverrideId: String +} + +enum Builder { + HEROKU + NIXPACKS + PAKETO +} + +type CnameCheck { + link: String + message: String! + status: CnameCheckStatus! +} + +enum CnameCheckStatus { + ERROR + INFO + INVALID + VALID + WAITING +} + +type CustomDomain implements Domain { + cnameCheck: CnameCheck! + createdAt: DateTime + deletedAt: DateTime + domain: String! + environmentId: String! + id: ID! + serviceId: String! + updatedAt: DateTime +} + +type CustomDomainAvailable { + available: Boolean! + message: String! +} + +input CustomDomainCreateInput { + domain: String! + environmentId: String! + serviceId: String! +} + +""" +A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. +""" +scalar DateTime + +type Deployment implements Node { + canRollback: Boolean! + createdAt: DateTime! + environmentId: String! + id: ID! + meta: DeploymentMeta + projectId: String! + serviceId: String + staticUrl: String + status: DeploymentStatus! + suggestAddServiceDomain: Boolean! + url: String +} + +scalar DeploymentMeta + +type DeploymentSnapshot implements Node { + createdAt: DateTime! + id: ID! + updatedAt: DateTime! +} + +enum DeploymentStatus { + BUILDING + CRASHED + DEPLOYING + FAILED + INITIALIZING + REMOVED + REMOVING + SKIPPED + SUCCESS + WAITING +} + +type DeploymentTrigger implements Node { + baseEnvironmentOverrideId: String + branch: String! + checkSuites: Boolean! + environmentId: String! + id: ID! + projectId: String! + provider: String! + repository: String! + serviceId: String + validCheckSuites: Int! +} + +input DeploymentTriggerCreateInput { + branch: String! + checkSuites: Boolean + environmentId: String! + projectId: String! + provider: String! + repository: String! + rootDirectory: String + serviceId: String! +} + +input DeploymentTriggerUpdateInput { + branch: String + checkSuites: Boolean + repository: String + rootDirectory: String +} + +interface Domain { + createdAt: DateTime + deletedAt: DateTime + domain: String! + environmentId: String! + id: ID! + serviceId: String! + updatedAt: DateTime +} + +type Environment implements Node { + createdAt: DateTime! + deletedAt: DateTime + deploymentTriggers( + after: String + before: String + first: Int + last: Int + ): EnvironmentDeploymentTriggersConnection! + deployments( + after: String + before: String + first: Int + last: Int + ): EnvironmentDeploymentsConnection! + id: ID! + isEphemeral: Boolean! + meta: EnvironmentMeta + name: String! + projectId: String! + serviceInstances( + after: String + before: String + first: Int + last: Int + ): EnvironmentServiceInstancesConnection! + updatedAt: DateTime! + variables( + after: String + before: String + first: Int + last: Int + ): EnvironmentVariablesConnection! +} + +input EnvironmentCreateInput { + name: String! + projectId: String! +} + +type EnvironmentDeploymentTriggersConnection { + edges: [EnvironmentDeploymentTriggersConnectionEdge!]! + pageInfo: PageInfo! +} + +type EnvironmentDeploymentTriggersConnectionEdge { + cursor: String! + node: DeploymentTrigger! +} + +type EnvironmentDeploymentsConnection { + edges: [EnvironmentDeploymentsConnectionEdge!]! + pageInfo: PageInfo! +} + +type EnvironmentDeploymentsConnectionEdge { + cursor: String! + node: Deployment! +} + +type EnvironmentMeta { + baseBranch: String + branch: String + prNumber: Int + prRepo: String + prTitle: String +} + +type EnvironmentServiceInstancesConnection { + edges: [EnvironmentServiceInstancesConnectionEdge!]! + pageInfo: PageInfo! +} + +type EnvironmentServiceInstancesConnectionEdge { + cursor: String! + node: ServiceInstance! +} + +input EnvironmentTriggersDeployInput { + environmentId: String! + projectId: String! + serviceId: String! +} + +type EnvironmentVariablesConnection { + edges: [EnvironmentVariablesConnectionEdge!]! + pageInfo: PageInfo! +} + +type EnvironmentVariablesConnectionEdge { + cursor: String! + node: Variable! +} + +""" +The estimated usage of a single measurement. +""" +type EstimatedUsage { + """ + The estimated value. + """ + estimatedValue: Float! + + """ + The measurement that was estimated. + """ + measurement: MetricMeasurement! + projectId: String! +} + +type Event implements Node { + action: String! + createdAt: DateTime! + environment: Environment + environmentId: String + id: ID! + object: String! + payload: JSON + project: Project! + projectId: String! +} + +input EventBatchTrackInput { + events: [EventTrackInput!]! +} + +scalar EventProperties + +input EventTrackInput { + eventName: String! + properties: EventProperties + ts: String! +} + +type ExecutionTime { + projectId: String! + + """ + The total number of minutes that the project has been actively running for. + """ + totalTimeMinutes: Float! +} + +input ExplicitOwnerInput { + """ + The ID of the owner + """ + id: String! + + """ + The type of owner + """ + type: ResourceOwnerType! +} + +type GitHubBranch { + name: String! +} + +type GitHubEvent { + createdAt: DateTime + type: String! +} + +type GitHubRepo { + defaultBranch: String! + fullName: String! + id: Int! + installationId: String! + isPrivate: Boolean! + name: String! +} + +input GitHubRepoUpdateInput { + environmentId: String! + projectId: String! + serviceId: String! +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON + +input JobApplicationCreateInput { + email: String! + jobId: String! + name: String! + resume: Upload! + why: String! +} + +type LockdownStatus { + allProvisionsDisabledMsg: String + anonProvisionsDisabledMsg: String + signupsDisabledMsg: String +} + +type Log { + message: String! + timestamp: String! +} + +""" +A single sample of a metric. +""" +type Metric { + """ + The timestamp of the sample. Represented has number of seconds since the Unix epoch. + """ + ts: Int! + + """ + The value of the sample. + """ + value: Float! +} + +""" +A thing that can be measured on Railway. +""" +enum MetricMeasurement { + CPU_USAGE + MEASUREMENT_UNSPECIFIED + MEMORY_USAGE_GB + NETWORK_RX_GB + NETWORK_TX_GB + UNRECOGNIZED +} + +""" +A property that can be used to group metrics. +""" +enum MetricTag { + DEPLOYMENT_ID + ENVIRONMENT_ID + KEY_UNSPECIFIED + PLUGIN_ID + PROJECT_ID + SERVICE_ID + UNRECOGNIZED +} + +""" +The tags that were used to group the metric. +""" +type MetricTags { + deploymentId: String + environmentId: String + pluginId: String + projectId: String + serviceId: String +} + +""" +The result of a metrics query. +""" +type MetricsResult { + """ + The measurement of the metric. + """ + measurement: MetricMeasurement! + + """ + The tags that were used to group the metric. Only the tags that were used to by will be present. + """ + tags: MetricTags! + + """ + The samples of the metric. + """ + values: [Metric!]! +} + +input MissingCommandAlertInput { + page: String! + text: String! +} + +type Mutation { + """ + Toggles all provisions on or off across the platform. + """ + allProvisionsToggle(input: TogglePlatformServiceInput!): Boolean! + + """ + Toggles anonymous provisions on or off across the platform. + """ + anonProvisionsToggle(input: TogglePlatformServiceInput!): Boolean! + + """ + Creates a new API token. + """ + apiTokenCreate(input: ApiTokenCreateInput!): String! + + """ + Deletes an API token. + """ + apiTokenDelete(id: String!): Boolean! + + """ + Sets the base environment override for a deployment trigger. + """ + baseEnvironmentOverride( + id: String! + input: BaseEnvironmentOverrideInput! + ): Boolean! + + """ + Creates a new custom domain. + """ + customDomainCreate(input: CustomDomainCreateInput!): CustomDomain! + + """ + Deletes a custom domain. + """ + customDomainDelete(id: String!): Boolean! + + """ + Cancels a deployment. + """ + deploymentCancel(id: String!): Boolean! + + """ + Redeploys a deployment. + """ + deploymentRedeploy(id: String!): Deployment! + + """ + Removes a deployment. + """ + deploymentRemove(id: String!): Boolean! + + """ + Restarts a deployment. + """ + deploymentRestart(id: String!): Boolean! + + """ + Rolls back to a deployment. + """ + deploymentRollback(id: String!): Boolean! + + """ + Creates a deployment trigger. + """ + deploymentTriggerCreate( + input: DeploymentTriggerCreateInput! + ): DeploymentTrigger! + + """ + Deletes a deployment trigger. + """ + deploymentTriggerDelete(id: String!): Boolean! + deploymentTriggerUpdate( + id: String! + input: DeploymentTriggerUpdateInput! + ): DeploymentTrigger! + + """ + Creates a new environment. + """ + environmentCreate(input: EnvironmentCreateInput!): Environment! + + """ + Deletes an environment. + """ + environmentDelete(id: String!): Boolean! + + """ + Deploys all connected triggers for an environment. + """ + environmentTriggersDeploy(input: EnvironmentTriggersDeployInput!): Boolean! + + """ + Track a batch of events for authenticated user + """ + eventBatchTrack(input: EventBatchTrackInput!): Boolean! + + """ + Track event for authenticated user + """ + eventTrack(input: EventTrackInput!): Boolean! + + """ + Updates a GitHub repo through the linked template + """ + githubRepoUpdate(input: GitHubRepoUpdateInput!): Boolean! + + """ + Creates a new job application. + """ + jobApplicationCreate(input: JobApplicationCreateInput!): Boolean! + + """ + Get a token for a login session if it exists + """ + loginSessionConsume(code: String!): String + + """ + Start a CLI login session + """ + loginSessionCreate: String! + + """ + Deletes session for current user if it exists + """ + logout: Boolean! + + """ + Alert the team of a missing command palette command + """ + missingCommandAlert(input: MissingCommandAlertInput!): Boolean! + + """ + Creates a new plugin. + """ + pluginCreate(input: PluginCreateInput!): Plugin! + + """ + Deletes a plugin. + """ + pluginDelete(id: String!): Boolean! + + """ + Restarts a plugin. + """ + pluginRestart(id: String!, input: PluginRestartInput!): Plugin! + + """ + Updates an existing plugin. + """ + pluginUpdate(id: String!, input: PluginUpdateInput!): Plugin! + preferencesUpdate(input: PreferencesUpdateData!): Preferences! + + """ + Claims a project. + """ + projectClaim(id: String!): Project! + + """ + Creates a new project. + """ + projectCreate(input: ProjectCreateInput!): Project! + + """ + Deletes a project. + """ + projectDelete(id: String!): Boolean! + + """ + Remove user from a project + """ + projectMemberRemove(input: ProjectMemberRemoveInput!): [ProjectMember!]! + + """ + Change the role for a user within a project + """ + projectMemberUpdate(input: ProjectMemberUpdateInput!): ProjectMember! + + """ + Create a token for a project that has access to a specific environment + """ + projectTokenCreate(input: ProjectTokenCreateInput!): String! + + """ + Delete a project token + """ + projectTokenDelete(id: String!): Boolean! + + """ + Confirm the transfer of project ownership + """ + projectTransferConfirm(input: ProjectTransferConfirmInput!): Boolean! + + """ + Initiate the transfer of project ownership + """ + projectTransferInitiate(input: ProjectTransferInitiateInput!): Boolean! + + """ + Updates a project. + """ + projectUpdate(id: String!, input: ProjectUpdateInput!): Project! + + """ + Deletes a ProviderAuth. + """ + providerAuthRemove(id: String!): Boolean! + + """ + Generates a new set of recovery codes for the authenticated user. + """ + recoveryCodeGenerate: RecoveryCodes! + + """ + Validates a recovery code. + """ + recoveryCodeValidate(input: RecoveryCodeValidateInput!): Boolean! + + """ + Updates the ReferralInfo for the authenticated user. + """ + referralInfoUpdate(input: ReferralInfoUpdateInput!): ReferralInfo! + + """ + Creates a new service. + """ + serviceCreate(input: ServiceCreateInput!): Service! + + """ + Deletes a service. + """ + serviceDelete(id: String!): Boolean! + + """ + Creates a new service domain. + """ + serviceDomainCreate(input: ServiceDomainCreateInput!): ServiceDomain! + + """ + Deletes a service domain. + """ + serviceDomainDelete(id: String!): Boolean! + + """ + Updates a service domain. + """ + serviceDomainUpdate(input: ServiceDomainUpdateInput!): Boolean! + + """ + Update a service instance + """ + serviceInstanceUpdate( + input: ServiceInstanceUpdateInput! + serviceId: String! + ): Boolean! + + """ + Updates a service. + """ + serviceUpdate(id: String!, input: ServiceUpdateInput!): Service! + sharedVariableConfigure(input: SharedVariableConfigureInput!): Variable! + + """ + Toggles signups on or off across the platform. + """ + signupsToggle(input: TogglePlatformServiceInput!): Boolean! + + """ + Creates a support request. + """ + supportRequest(input: SupportRequestInput!): Boolean! + + """ + Bans a team. + """ + teamBan(id: String!, input: TeamBanInput!): Boolean! + + """ + Changes a user team permissions. + """ + teamPermissionChange(input: TeamPermissionChangeInput!): Boolean! + + """ + Stops all deployments and plugins for a team. + """ + teamResourcesStop(id: String!, input: TeamResourcesStopInput): Boolean! + + """ + Unbans a team. + """ + teamUnban(id: String!): Boolean! + + """ + Logs panics from CLI to Datadog + """ + telemetrySend(input: TelemetrySendInput!): Boolean! + + """ + Creates a template. + """ + templateCreate(input: TemplateCreateInput!): Template! + + """ + Deletes a template. + """ + templateDelete(id: String!): Boolean! + + """ + Deploys a template. + """ + templateDeploy(input: TemplateDeployInput!): TemplateDeployPayload! + + """ + Publishes a template. + """ + templatePublish(id: String!, input: TemplatePublishInput!): Boolean! + + """ + Unpublishes a template. + """ + templateUnpublish(id: String!): Boolean! + + """ + Updates a template. + """ + templateUpdate(id: String!, input: TemplateUpdateInput!): Template! + + """ + Setup 2FA authorization for authenticated user. + """ + twoFactorInfoCreate(input: TwoFactorInfoCreateInput!): RecoveryCodes! + + """ + Deletes the TwoFactorInfo for the authenticated user. + """ + twoFactorInfoDelete: Boolean! + + """ + Generates the 2FA app secret for the authenticated user. + """ + twoFactorInfoSecret: TwoFactorInfoSecret! + + """ + Validates the token for a 2FA action or for a login request. + """ + twoFactorInfoValidate(input: TwoFactorInfoValidateInput!): Boolean! + + """ + Set flags on the authenticated user. + """ + userFlagsSet(input: UserFlagsSetInput!): Boolean! + + """ + Upserts a collection of variables. + """ + variableCollectionUpsert(input: VariableCollectionUpsertInput!): Boolean! + + """ + Deletes a variable. + """ + variableDelete(input: VariableDeleteInput!): Boolean! + + """ + Upserts a variable. + """ + variableUpsert(input: VariableUpsertInput!): Boolean! +} + +interface Node { + id: ID! +} + +type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String +} + +enum PlatformServiceStatus { + DISABLE + ENABLE +} + +type Plugin implements Node { + createdAt: DateTime! + deletedAt: DateTime + friendlyName: String! + id: ID! + logsEnabled: Boolean! + name: PluginType! + project: Project! + status: PluginStatus! + variables( + after: String + before: String + first: Int + last: Int + ): PluginVariablesConnection! +} + +input PluginCreateInput { + name: String! + projectId: String! +} + +input PluginRestartInput { + environmentId: String! +} + +enum PluginStatus { + LOCKED + REMOVED + RUNNING + STOPPED +} + +enum PluginType { + mongodb + mysql + postgresql + redis +} + +input PluginUpdateInput { + friendlyName: String! +} + +type PluginVariablesConnection { + edges: [PluginVariablesConnectionEdge!]! + pageInfo: PageInfo! +} + +type PluginVariablesConnectionEdge { + cursor: String! + node: Variable! +} + +type Preferences implements Node { + buildFailedEmail: Boolean! + changelogEmail: Boolean! + deployCrashedEmail: Boolean! + id: ID! + marketingEmail: Boolean! + usageEmail: Boolean! +} + +input PreferencesUpdateData { + buildFailedEmail: Boolean + changelogEmail: Boolean + deployCrashedEmail: Boolean + marketingEmail: Boolean + token: String + usageEmail: Boolean +} + +type Project implements Node { + createdAt: DateTime! + deletedAt: DateTime + deploymentTriggers( + after: String + before: String + first: Int + last: Int + ): ProjectDeploymentTriggersConnection! + deployments( + after: String + before: String + first: Int + last: Int + ): ProjectDeploymentsConnection! + description: String + environments( + after: String + before: String + first: Int + last: Int + ): ProjectEnvironmentsConnection! + expiredAt: DateTime + id: ID! + isPublic: Boolean! + isTempProject: Boolean! + isUpdatable: Boolean! + members: [ProjectMember!]! + name: String! + plugins( + after: String + before: String + first: Int + last: Int + ): ProjectPluginsConnection! + prDeploys: Boolean! + projectPermissions( + after: String + before: String + first: Int + last: Int + ): ProjectProjectPermissionsConnection! + services( + after: String + before: String + first: Int + last: Int + ): ProjectServicesConnection! + team: Team + teamId: String + updatedAt: DateTime! + upstreamUrl: String + webhooks( + after: String + before: String + first: Int + last: Int + ): ProjectWebhooksConnection! +} + +input ProjectCreateInput { + description: String + isPublic: Boolean + name: String + plugins: [String!] + prDeploys: Boolean + repo: ProjectCreateRepo + teamId: String +} + +input ProjectCreateRepo { + branch: String! + fullRepoName: String! +} + +type ProjectDeploymentTriggersConnection { + edges: [ProjectDeploymentTriggersConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectDeploymentTriggersConnectionEdge { + cursor: String! + node: DeploymentTrigger! +} + +type ProjectDeploymentsConnection { + edges: [ProjectDeploymentsConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectDeploymentsConnectionEdge { + cursor: String! + node: Deployment! +} + +type ProjectEnvironmentsConnection { + edges: [ProjectEnvironmentsConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectEnvironmentsConnectionEdge { + cursor: String! + node: Environment! +} + +type ProjectMember { + avatar: String + email: String! + id: String! + name: String + role: ProjectRole! +} + +input ProjectMemberRemoveInput { + projectId: String! + userId: String! +} + +input ProjectMemberUpdateInput { + projectId: String! + role: ProjectRole! + userId: String! +} + +type ProjectPermission implements Node { + id: ID! + projectId: String! + role: ProjectRole! + userId: String! +} + +type ProjectPluginsConnection { + edges: [ProjectPluginsConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectPluginsConnectionEdge { + cursor: String! + node: Plugin! +} + +type ProjectProjectPermissionsConnection { + edges: [ProjectProjectPermissionsConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectProjectPermissionsConnectionEdge { + cursor: String! + node: ProjectPermission! +} + +type ProjectResourceAccess { + customDomain: AccessRule! + deployment: AccessRule! + environment: AccessRule! + plugin: AccessRule! +} + +enum ProjectRole { + ADMIN + MEMBER + VIEWER +} + +type ProjectServicesConnection { + edges: [ProjectServicesConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectServicesConnectionEdge { + cursor: String! + node: Service! +} + +type ProjectToken implements Node { + createdAt: DateTime! + displayToken: String! + environment: Environment! + environmentId: String! + id: ID! + name: String! + project: Project! + projectId: String! +} + +input ProjectTokenCreateInput { + environmentId: String! + name: String! + projectId: String! +} + +input ProjectTransferConfirmInput { + ownershipTransferId: String! + projectId: String! +} + +input ProjectTransferInitiateInput { + memberId: String! + projectId: String! +} + +input ProjectUpdateInput { + description: String + isPublic: Boolean + name: String + prDeploys: Boolean +} + +type ProjectWebhook implements Node { + id: ID! + projectId: String! + url: String! +} + +type ProjectWebhooksConnection { + edges: [ProjectWebhooksConnectionEdge!]! + pageInfo: PageInfo! +} + +type ProjectWebhooksConnectionEdge { + cursor: String! + node: ProjectWebhook! +} + +type ProviderAuth implements Node { + email: String! + id: ID! + metadata: JSON! + provider: String! + userId: String! +} + +type PublicStats { + totalDeployments: Int! + totalProjects: Int! + totalUsers: Int! +} + +type Query { + apiTokens( + after: String + before: String + first: Int + last: Int + ): QueryApiTokensConnection! + + """ + Gets the ban reason history for a user. + """ + banReasonHistory( + after: String + before: String + first: Int + last: Int + userId: String! + ): QueryBanReasonHistoryConnection! + + """ + Fetch logs for a build + """ + buildLogs( + deploymentId: String! + endDate: DateTime + + """ + Filter logs by a string. Providing an empty value will match all logs. + """ + filter: String + + """ + Limit the number of logs returned. Defaults to 100. + """ + limit: Int! = 100 + startDate: DateTime + ): [Log!]! + changelogBlockImage(id: String!): String! + + """ + Checks if a custom domain is available. + """ + customDomainAvailable(domain: String!): CustomDomainAvailable! + + """ + Fetch logs for a deployment + """ + deploymentLogs( + deploymentId: String! + endDate: DateTime + + """ + Filter logs by a string. Providing an empty value will match all logs. + """ + filter: String + + """ + Limit the number of logs returned. Defaults to 100. + """ + limit: Int! = 100 + startDate: DateTime + ): [Log!]! + + """ + Get a short-lived URL to the deployment snapshot code + """ + deploymentSnapshotCodeUri(deploymentId: String!): String! + + """ + All deployment triggers. + """ + deploymentTriggers( + after: String + before: String + environmentId: String! + first: Int + last: Int + projectId: String! + serviceId: String! + ): QueryDeploymentTriggersConnection! + + """ + All domains + """ + domains( + environmentId: String! + projectId: String! + serviceId: String! + ): AllDomains! + + """ + Get the estimated total cost of the project at the end of the current billing cycle + """ + estimatedUsage( + """ + Whether to include deleted projects in estimations. + """ + includeDeleted: Boolean + measurements: [MetricMeasurement!]! + projectId: String + teamId: String + userId: String + ): [EstimatedUsage!]! + events( + after: String + before: String + environmentId: String + first: Int + last: Int + projectId: String! + ): QueryEventsConnection! + + """ + Get the execution time of projects + """ + executionTime( + """ + Whether to get execution for deleted projects. + """ + includeDeleted: Boolean + projectId: String + teamId: String + userId: String + ): [ExecutionTime!]! + + """ + Get GitHub events for a user + """ + githubEvents(userId: String!): [GitHubEvent!]! + + """ + Check if a repo name is available + """ + githubIsRepoNameAvailable(fullRepoName: String!): Boolean! + + """ + Get branches for a GitHub repo that the authenticated user has access to + """ + githubRepoBranches(owner: String!, repo: String!): [GitHubBranch!]! + + """ + Get a list of repos for a user that Railway has access to + """ + githubRepos: [GitHubRepo!]! + + """ + Get a list of scopes the user has installed the installation to + """ + githubWritableScopes: [String!]! + + """ + Returns the current lockdown status of the platform. + """ + lockdownStatus: LockdownStatus! + me: User! + + """ + Get metrics for a project, environment, and service + """ + metrics( + """ + The averaging window when computing CPU usage. By default, it is the same as the `sampleRateSeconds`. + """ + averagingWindowSeconds: Int + + """ + The end of the period to get metrics for. If not provided, the current datetime is used. + """ + endDate: DateTime + environmentId: String + + """ + What to group the aggregated usage by. By default, it is grouped over the entire project. + """ + groupBy: [MetricTag!] + + """ + Whether or not to include deleted projects in the results + """ + includeDeleted: Boolean + measurements: [MetricMeasurement!]! + pluginId: String + projectId: String + + """ + The frequency of data points in the response. If the `sampleRateSeconds` is 60, then the response will contain one data point per minute. + """ + sampleRateSeconds: Int + serviceId: String + + """ + The start of the period to get metrics for. + """ + startDate: DateTime! + teamId: String + userId: String + ): [MetricsResult!]! + node(id: ID!): Node + nodes(ids: [ID!]!): [Node]! + + """ + Fetch logs for a plugin + """ + pluginLogs( + endDate: DateTime + environmentId: String! + + """ + Filter logs by a string. Providing an empty value will match all logs. + """ + filter: String + + """ + Limit the number of logs returned. Defaults to 100. + """ + limit: Int! = 100 + pluginId: String! + startDate: DateTime + ): [Log!]! + preferences(token: String): Preferences! + project(id: String!): Project! + + """ + Gets users who belong to a project along with their role + """ + projectMembers(projectId: String!): [ProjectMember!]! + + """ + Get resource access rules for project-specific actions + """ + projectResourceAccess(projectId: String!): ProjectResourceAccess! + + """ + Get a single project token by the value in the header + """ + projectToken: ProjectToken! + + """ + Get all project tokens for a project + """ + projectTokens( + after: String + before: String + first: Int + last: Int + projectId: String! + ): QueryProjectTokensConnection! + projects( + after: String + before: String + first: Int + includeDeleted: Boolean + last: Int + teamId: String + userId: String + ): QueryProjectsConnection! + + """ + Get public Railway stats. Primarily used for the landing page. + """ + publicStats: PublicStats! + referralInfo: ReferralInfo! + + """ + Get resource access for the current user or team + """ + resourceAccess(explicitResourceOwner: ExplicitOwnerInput): ResourceAccess! + + """ + Suggested service domain + """ + suggestedServiceDomain( + environmentId: String! + projectId: String! + serviceId: String! + ): String! + template(code: String, owner: String, repo: String): Template! + + """ + Gets the README for a template. + """ + templateReadme(code: String!): TemplateReadme! + templates( + after: String + before: String + first: Int + last: Int + ): QueryTemplatesConnection! + + """ + Gets the TwoFactorInfo for the authenticated user. + """ + twoFactorInfo: TwoFactorInfo! + + """ + Get the usage for a single project or all projects for a user/team. If no `projectId` or `teamId` is provided, the usage for the current user is returned. + """ + usage( + endDate: DateTime + + """ + What to group the aggregated usage by. By default, it is grouped over the entire project. + """ + groupBy: [MetricTag!] + + """ + Whether to include deleted projects in the usage. + """ + includeDeleted: Boolean + measurements: [MetricMeasurement!]! + projectId: String + startDate: DateTime + teamId: String + userId: String + ): [AggregatedUsage!]! + userTemplates( + after: String + before: String + first: Int + last: Int + ): QueryUserTemplatesConnection! + + """ + All variables by pluginId or serviceId. If neither are provided, all shared variables are returned. + """ + variables( + environmentId: String! + + """ + Provide a pluginId to get all variables for a specific plugin. + """ + pluginId: String + projectId: String! + + """ + Provide a serviceId to get all variables for a specific service. + """ + serviceId: String + unrendered: Boolean + ): ServiceVariables! +} + +type QueryApiTokensConnection { + edges: [QueryApiTokensConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryApiTokensConnectionEdge { + cursor: String! + node: ApiToken! +} + +type QueryBanReasonHistoryConnection { + edges: [QueryBanReasonHistoryConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryBanReasonHistoryConnectionEdge { + cursor: String! + node: BanReasonHistory! +} + +type QueryDeploymentTriggersConnection { + edges: [QueryDeploymentTriggersConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryDeploymentTriggersConnectionEdge { + cursor: String! + node: DeploymentTrigger! +} + +type QueryEventsConnection { + edges: [QueryEventsConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryEventsConnectionEdge { + cursor: String! + node: Event! +} + +type QueryProjectTokensConnection { + edges: [QueryProjectTokensConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryProjectTokensConnectionEdge { + cursor: String! + node: ProjectToken! +} + +type QueryProjectsConnection { + edges: [QueryProjectsConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryProjectsConnectionEdge { + cursor: String! + node: Project! +} + +type QueryTemplatesConnection { + edges: [QueryTemplatesConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryTemplatesConnectionEdge { + cursor: String! + node: Template! +} + +type QueryUserTemplatesConnection { + edges: [QueryUserTemplatesConnectionEdge!]! + pageInfo: PageInfo! +} + +type QueryUserTemplatesConnectionEdge { + cursor: String! + node: Template! +} + +input RecoveryCodeValidateInput { + code: String! + twoFactorLinkingKey: String +} + +type RecoveryCodes { + recoveryCodes: [String!]! +} + +type ReferralInfo implements Node { + code: String! + id: ID! + referralStats: ReferralStats! + status: String! +} + +input ReferralInfoUpdateInput { + code: String! +} + +type ReferralStats { + credited: Int! + pending: Int! +} + +type ResourceAccess { + project: AccessRule! +} + +enum ResourceOwnerType { + TEAM + USER +} + +enum RestartPolicyType { + ALWAYS + NEVER + ON_FAILURE +} + +type Service implements Node { + createdAt: DateTime! + deletedAt: DateTime + deployments( + after: String + before: String + first: Int + last: Int + ): ServiceDeploymentsConnection! + icon: String + id: ID! + name: String! + project: Project! + projectId: String! + repoTriggers( + after: String + before: String + first: Int + last: Int + ): ServiceRepoTriggersConnection! + serviceInstances( + after: String + before: String + first: Int + last: Int + ): ServiceServiceInstancesConnection! + updatedAt: DateTime! +} + +input ServiceCreateInput { + branch: String + name: String + projectId: String! + source: ServiceSourceInput + variables: ServiceVariables +} + +type ServiceDeploymentsConnection { + edges: [ServiceDeploymentsConnectionEdge!]! + pageInfo: PageInfo! +} + +type ServiceDeploymentsConnectionEdge { + cursor: String! + node: Deployment! +} + +type ServiceDomain implements Domain { + createdAt: DateTime + deletedAt: DateTime + domain: String! + environmentId: String! + id: ID! + serviceId: String! + suffix: String + updatedAt: DateTime +} + +input ServiceDomainCreateInput { + environmentId: String! + serviceId: String! +} + +input ServiceDomainUpdateInput { + domain: String! + environmentId: String! + serviceId: String! +} + +type ServiceInstance implements Node { + buildCommand: String + builder: Builder! + createdAt: DateTime! + deletedAt: DateTime + domains: AllDomains! + environmentId: String! + healthcheckPath: String + healthcheckTimeout: Int + id: ID! + isUpdatable: Boolean! + nixpacksPlan: JSON + railwayConfigFile: String + restartPolicyMaxRetries: Int! + restartPolicyType: RestartPolicyType! + rootDirectory: String + serviceId: String! + source: ServiceSource + startCommand: String + updatedAt: DateTime! + upstreamUrl: String + watchPatterns: [String!]! +} + +input ServiceInstanceUpdateInput { + buildCommand: String + builder: Builder + healthcheckPath: String + healthcheckTimeout: Int + nixpacksPlan: JSON + railwayConfigFile: String + restartPolicyMaxRetries: Int + restartPolicyType: RestartPolicyType + rootDirectory: String + source: ServiceSourceInput + startCommand: String + watchPatterns: [String!] +} + +type ServiceRepoTriggersConnection { + edges: [ServiceRepoTriggersConnectionEdge!]! + pageInfo: PageInfo! +} + +type ServiceRepoTriggersConnectionEdge { + cursor: String! + node: DeploymentTrigger! +} + +type ServiceServiceInstancesConnection { + edges: [ServiceServiceInstancesConnectionEdge!]! + pageInfo: PageInfo! +} + +type ServiceServiceInstancesConnectionEdge { + cursor: String! + node: ServiceInstance! +} + +type ServiceSource { + repo: String + template: TemplateServiceSource +} + +input ServiceSourceInput { + repo: String! +} + +input ServiceUpdateInput { + icon: String + name: String +} + +""" +The ServiceVariables scalar type represents values as the TypeScript type: Record. Example: "{ foo: 'bar', baz: 'qux' }" +""" +scalar ServiceVariables + +input SharedVariableConfigureInput { + disabledServiceIds: [String!]! + enabledServiceIds: [String!]! + environmentId: String! + name: String! + projectId: String! +} + +type SimilarTemplate { + code: String! + deploys: Int! + description: String + name: String! +} + +type Subscription { + """ + Stream logs for a build + """ + buildLogs( + deploymentId: String! + + """ + Filter logs by a string. Providing an empty value will match all logs. + """ + filter: String + + """ + Limit the number of logs returned. Defaults to 100. + """ + limit: Int! = 100 + ): [Log!]! + + """ + Stream logs for a deployment + """ + deploymentLogs( + deploymentId: String! + + """ + Filter logs by a string. Providing an empty value will match all logs. + """ + filter: String + + """ + Limit the number of logs returned. Defaults to 100. + """ + limit: Int! = 100 + ): [Log!]! + + """ + Stream logs for a plugin + """ + pluginLogs( + environmentId: String! + + """ + Filter logs by a string. Providing an empty value will match all logs. + """ + filter: String + + """ + Limit the number of logs returned. Defaults to 100. + """ + limit: Int! = 100 + pluginId: String! + ): [Log!]! +} + +input SupportRequestInput { + isPurchasing: Boolean + isTechnical: Boolean + text: String! +} + +type Team implements Node { + avatar: String + id: ID! + name: String! +} + +input TeamBanInput { + banReason: String! +} + +type TeamPermission implements Node { + createdAt: DateTime! + id: ID! + role: TeamRole! + teamId: String! + updatedAt: DateTime! + userId: String! +} + +input TeamPermissionChangeInput { + role: TeamRole! + teamId: String! + userId: String! +} + +input TeamResourcesStopInput { + reason: String! +} + +enum TeamRole { + ADMIN + MEMBER +} + +input TelemetrySendInput { + command: String! + environmentId: String + error: String! + projectId: String + stacktrace: String! + version: String +} + +type Template implements Node { + code: String! + config: TemplateConfig! + createdAt: DateTime! + creator: TemplateCreator + demoProjectId: String + id: ID! + metadata: TemplateMetadata! + projects: Int! + services( + after: String + before: String + first: Int + last: Int + ): TemplateServicesConnection! + similarTemplates: [SimilarTemplate!]! + status: TemplateStatus! + userId: String +} + +scalar TemplateConfig + +input TemplateCreateInput { + config: TemplateConfig! + demoProjectId: String + metadata: TemplateMetadata! + services: [TemplateServiceCreateInput!]! +} + +type TemplateCreator { + avatar: String + name: String +} + +input TemplateDeployInput { + plugins: [String!] + projectId: String + services: [TemplateDeployService!]! + teamId: String + templateCode: String +} + +type TemplateDeployPayload { + projectId: String! + workflowId: String! +} + +input TemplateDeployService { + commit: String + hasDomain: Boolean + healthcheckPath: String + id: String + isPrivate: Boolean + name: String! + owner: String! + rootDirectory: String + serviceName: String! + startCommand: String + template: String! + variables: ServiceVariables +} + +scalar TemplateMetadata + +input TemplatePublishInput { + category: String! + description: String! + image: String + readme: String! +} + +type TemplateReadme { + description: String + name: String! + readmeContent: String! +} + +type TemplateService implements Node { + config: TemplateServiceConfig! + createdAt: DateTime! + id: ID! + templateId: String! + updatedAt: DateTime! +} + +scalar TemplateServiceConfig + +input TemplateServiceCreateInput { + config: TemplateServiceConfig! +} + +type TemplateServiceSource { + serviceName: String! + serviceSource: String! +} + +input TemplateServiceUpdateInput { + config: TemplateServiceConfig! + id: String +} + +type TemplateServicesConnection { + edges: [TemplateServicesConnectionEdge!]! + pageInfo: PageInfo! +} + +type TemplateServicesConnectionEdge { + cursor: String! + node: TemplateService! +} + +enum TemplateStatus { + HIDDEN + PUBLISHED + UNPUBLISHED +} + +input TemplateUpdateInput { + config: TemplateConfig! + demoProjectId: String + metadata: TemplateMetadata! + services: [TemplateServiceUpdateInput!]! +} + +input TogglePlatformServiceInput { + reason: String + status: PlatformServiceStatus! +} + +type TwoFactorInfo { + hasRecoveryCodes: Boolean! + isVerified: Boolean! +} + +input TwoFactorInfoCreateInput { + token: String! +} + +type TwoFactorInfoSecret { + secret: String! + uri: String! +} + +input TwoFactorInfoValidateInput { + token: String! + twoFactorLinkingKey: String +} + +""" +The `Upload` scalar type represents a file upload. +""" +scalar Upload + +type User implements Node { + email: String! + id: ID! + name: String + projects( + after: String + before: String + first: Int + last: Int + ): UserProjectsConnection! + teams( + after: String + before: String + first: Int + last: Int + ): UserTeamsConnection! +} + +enum UserFlag { + API_PREVIEW + BETA +} + +input UserFlagsSetInput { + flags: [UserFlag!]! +} + +type UserProjectsConnection { + edges: [UserProjectsConnectionEdge!]! + pageInfo: PageInfo! +} + +type UserProjectsConnectionEdge { + cursor: String! + node: Project! +} + +type UserTeamsConnection { + edges: [UserTeamsConnectionEdge!]! + pageInfo: PageInfo! +} + +type UserTeamsConnectionEdge { + cursor: String! + node: Team! +} + +type Variable implements Node { + createdAt: DateTime! + environment: Environment! + environmentId: String + id: ID! + name: String! + plugin: Plugin! + pluginId: String + service: Service! + serviceId: String + updatedAt: DateTime! +} + +input VariableCollectionUpsertInput { + environmentId: String! + projectId: String! + + """ + When set to true, removes all existing variables before upserting the new collection. + """ + replace: Boolean = false + serviceId: String + variables: ServiceVariables! +} + +input VariableDeleteInput { + environmentId: String! + name: String! + projectId: String! + serviceId: String +} + +input VariableUpsertInput { + environmentId: String! + name: String! + projectId: String! + serviceId: String + value: String! +} diff --git a/src/gql/subscriptions/mod.rs b/src/gql/subscriptions/mod.rs new file mode 100644 index 0000000..91d81ba --- /dev/null +++ b/src/gql/subscriptions/mod.rs @@ -0,0 +1,17 @@ +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/subscriptions/strings/BuildLogs.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct BuildLogs; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/gql/schema.graphql", + query_path = "src/gql/subscriptions/strings/DeploymentLogs.graphql", + response_derives = "Debug, Serialize, Clone" +)] +pub struct DeploymentLogs; diff --git a/src/gql/subscriptions/strings/BuildLogs.graphql b/src/gql/subscriptions/strings/BuildLogs.graphql new file mode 100644 index 0000000..e75cacc --- /dev/null +++ b/src/gql/subscriptions/strings/BuildLogs.graphql @@ -0,0 +1,10 @@ +subscription BuildLogs($deploymentId: String!, $filter: String, $limit: Int) { + buildLogs(deploymentId: $deploymentId, filter: $filter, limit: $limit) { + ...LogFields + } +} + +fragment LogFields on Log { + timestamp + message +} diff --git a/src/gql/subscriptions/strings/DeploymentLogs.graphql b/src/gql/subscriptions/strings/DeploymentLogs.graphql new file mode 100644 index 0000000..00b1ee9 --- /dev/null +++ b/src/gql/subscriptions/strings/DeploymentLogs.graphql @@ -0,0 +1,14 @@ +subscription DeploymentLogs( + $deploymentId: String! + $filter: String + $limit: Int +) { + deploymentLogs(deploymentId: $deploymentId, filter: $filter, limit: $limit) { + ...LogFields + } +} + +fragment LogFields on Log { + timestamp + message +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..8a2a1dd --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,35 @@ +#[macro_export] +macro_rules! commands_enum { + ($($module:tt),*) => ( + paste::paste! { + #[derive(Subcommand)] + enum Commands { + $( + [<$module:camel>]($module::Args), + )* + } + + impl Commands { + async fn exec(cli: Args) -> Result<()> { + match cli.command { + $( + Commands::[<$module:camel>](args) => $module::command(args, cli.json).await?, + )* + } + Ok(()) + } + } + } + ); +} + +// Macro that bails if not running in a terminal +#[macro_export] +macro_rules! interact_or { + ($message:expr) => { + use anyhow::bail; + if !std::io::stdout().is_terminal() { + bail!($message); + } + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4a5fa93 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,65 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; + +mod commands; +use commands::*; + +mod client; +mod config; +mod consts; +mod gql; +mod subscription; +mod table; +mod util; + +#[macro_use] +mod macros; + +/// Interact with 🚅 Railway via CLI +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Args { + #[clap(subcommand)] + command: Commands, + + /// Output in JSON format + #[clap(global = true, long)] + json: bool, +} + +// Generates the commands based on the modules in the commands directory +// Specify the modules you want to include in the commands_enum! macro +commands_enum!( + add, + completion, + delete, + domain, + docs, + environment, + init, + link, + list, + login, + logout, + logs, + open, + run, + service, + shell, + starship, + status, + unlink, + up, + variables, + whoami +); + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Args::parse(); + + Commands::exec(cli).await?; + + Ok(()) +} diff --git a/src/subscription.rs b/src/subscription.rs new file mode 100644 index 0000000..cf40b80 --- /dev/null +++ b/src/subscription.rs @@ -0,0 +1,52 @@ +use crate::{commands::Configs, util::tokio_spawner::TokioSpawner}; +use anyhow::{bail, Result}; +use async_tungstenite::tungstenite::Message; +use graphql_client::GraphQLQuery; +use graphql_ws_client::{ + graphql::{GraphQLClient, StreamingOperation}, + AsyncWebsocketClient, SubscriptionStream, +}; + +pub async fn subscribe_graphql( + variables: T::Variables, +) -> Result<( + AsyncWebsocketClient, + SubscriptionStream>, +)> +where + ::Variables: Send + Sync + Unpin, + ::ResponseData: std::fmt::Debug, +{ + let configs = Configs::new()?; + + use async_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue}; + use futures::StreamExt; + use graphql_ws_client::GraphQLClientClientBuilder; + let Some(token) = configs.root_config.user.token.clone() else { + bail!("Unauthorized. Please login with `railway login`") + }; + let bearer = format!("Bearer {token}"); + let hostname = configs.get_host(); + let mut request = format!("wss://backboard.{hostname}/graphql/v2").into_client_request()?; + + request.headers_mut().insert( + "Sec-WebSocket-Protocol", + HeaderValue::from_str("graphql-transport-ws").unwrap(), + ); + request + .headers_mut() + .insert("Authorization", HeaderValue::from_str(&bearer)?); + + let (connection, _) = async_tungstenite::tokio::connect_async(request).await?; + + let (sink, stream) = connection.split::(); + + let mut client = GraphQLClientClientBuilder::new() + .build(stream, sink, TokioSpawner::current()) + .await?; + let stream = client + .streaming_operation(StreamingOperation::::new(variables)) + .await?; + + Ok((client, stream)) +} diff --git a/src/table.rs b/src/table.rs new file mode 100644 index 0000000..44ea811 --- /dev/null +++ b/src/table.rs @@ -0,0 +1,176 @@ +use anyhow::Result; +use colored::Colorize; +use indoc::formatdoc; +use std::collections::BTreeMap; + +const FIRST_COLUMN_MIN_WIDTH: usize = 10; +const MIN_BOX_WIDTH: usize = 20; +const MAX_BOX_WIDTH: usize = 80; + +pub struct Table { + name: String, + rows: BTreeMap, +} + +impl Table { + pub fn new(name: String, rows: BTreeMap) -> Self { + Self { name, rows } + } + pub fn get_string(&self) -> Result { + let title_str = format!(" Variables for {} ", self.name); + let title_width = console::measure_text_width(title_str.as_str()); + + let max_right_content = self + .rows + .iter() + .flat_map(|(_, content)| { + content + .split('\n') + .map(console::measure_text_width) + .collect::>() + }) + .max() + .unwrap_or(0); + let max_right_content = std::cmp::max(max_right_content, title_width); + let first_column_width = std::cmp::max( + FIRST_COLUMN_MIN_WIDTH, + self.rows + .keys() + .map(|name| console::measure_text_width(name)) + .max() + .unwrap_or(0), + ); + + let edge = format!("{} ", box_drawing::double::VERTICAL); + let edge_width = console::measure_text_width(edge.as_str()); + + let middle_padding = format!(" {} ", box_drawing::light::VERTICAL); + let middle_padding_width = console::measure_text_width(middle_padding.as_str()); + let middle_padding = middle_padding.cyan().dimmed().to_string(); + + let box_width = + ((edge_width * 2) + first_column_width + middle_padding_width + max_right_content) + .clamp(MIN_BOX_WIDTH, MAX_BOX_WIDTH); + + let second_column_width = + box_width - (edge_width * 2) - first_column_width - middle_padding_width; + + let title_side_padding = ((box_width as f64) - (title_width as f64) - 2.0) / 2.0; + + let top_box = format!( + "{}{}{}{}{}", + box_drawing::double::DOWN_RIGHT.cyan().dimmed(), + str::repeat( + box_drawing::double::HORIZONTAL, + title_side_padding.ceil() as usize + ) + .cyan() + .dimmed(), + title_str.magenta().bold(), + str::repeat( + box_drawing::double::HORIZONTAL, + title_side_padding.floor() as usize + ) + .cyan() + .dimmed(), + box_drawing::double::DOWN_LEFT.cyan().dimmed(), + ); + + let bottom_box = format!( + "{}{}{}", + box_drawing::double::UP_RIGHT.cyan().dimmed(), + str::repeat(box_drawing::double::HORIZONTAL, box_width - 2) + .cyan() + .dimmed(), + box_drawing::double::UP_LEFT.cyan().dimmed() + ); + + let hor_sep = format!( + "{}{}{}", + box_drawing::double::VERTICAL.cyan().dimmed(), + str::repeat(box_drawing::light::HORIZONTAL, box_width - 2) + .cyan() + .dimmed(), + box_drawing::double::VERTICAL.cyan().dimmed() + ); + + let phase_rows = self + .rows + .clone() + .into_iter() + .map(|(name, content)| { + print_row( + name.as_str(), + content.as_str(), + edge.as_str(), + middle_padding.as_str(), + first_column_width, + second_column_width, + false, + ) + }) + .collect::>() + .join(format!("\n{hor_sep}\n").as_str()); + + Ok(formatdoc! {" + {} + {} + {} + ", + top_box, + phase_rows, + bottom_box + }) + } + pub fn print(&self) -> Result<()> { + println!("{}", self.get_string()?); + Ok(()) + } +} + +fn print_row( + title: &str, + content: &str, + left_edge: &str, + middle: &str, + first_column_width: usize, + second_column_width: usize, + indent_second_line: bool, +) -> String { + let mut textwrap_opts = textwrap::Options::new(second_column_width); + textwrap_opts.break_words = true; + if indent_second_line { + textwrap_opts.subsequent_indent = " "; + } + + let right_edge = left_edge.chars().rev().collect::(); + + let list_lines = textwrap::wrap(content, textwrap_opts); + let mut output = format!( + "{}{}{}{}{}", + left_edge.cyan().dimmed(), + console::pad_str(title, first_column_width, console::Alignment::Left, None).bold(), + middle, + console::pad_str( + &list_lines[0], + second_column_width, + console::Alignment::Left, + None + ), + right_edge.cyan().dimmed() + ); + + for line in list_lines.iter().skip(1) { + output = format!( + "{}\n{}{}{}{}{}", + output, + left_edge.cyan().dimmed(), + console::pad_str("", first_column_width, console::Alignment::Left, None), + middle, + console::pad_str(line, second_column_width, console::Alignment::Left, None), + right_edge.cyan().dimmed() + ); + } + + output +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..07ddecc --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,2 @@ +pub mod prompt; +pub mod tokio_spawner; diff --git a/src/util/prompt.rs b/src/util/prompt.rs new file mode 100644 index 0000000..ec5b896 --- /dev/null +++ b/src/util/prompt.rs @@ -0,0 +1,28 @@ +use std::fmt::Display; + +use crate::commands::Configs; +use anyhow::{Context, Result}; + +pub fn prompt_options(message: &str, options: Vec) -> Result { + let select = inquire::Select::new(message, options); + select + .with_render_config(Configs::get_render_config()) + .prompt() + .context("Failed to prompt for options") +} + +pub fn prompt_confirm(message: &str) -> Result { + let confirm = inquire::Confirm::new(message); + confirm + .with_render_config(Configs::get_render_config()) + .prompt() + .context("Failed to prompt for confirm") +} + +pub fn prompt_multi_options(message: &str, options: Vec) -> Result> { + let multi_select = inquire::MultiSelect::new(message, options); + multi_select + .with_render_config(Configs::get_render_config()) + .prompt() + .context("Failed to prompt for multi options") +} diff --git a/src/util/tokio_spawner.rs b/src/util/tokio_spawner.rs new file mode 100644 index 0000000..e30e6ec --- /dev/null +++ b/src/util/tokio_spawner.rs @@ -0,0 +1,21 @@ +pub struct TokioSpawner(tokio::runtime::Handle); + +impl TokioSpawner { + pub fn new(handle: tokio::runtime::Handle) -> Self { + TokioSpawner(handle) + } + + pub fn current() -> Self { + TokioSpawner::new(tokio::runtime::Handle::current()) + } +} + +impl futures::task::Spawn for TokioSpawner { + fn spawn_obj( + &self, + obj: futures::task::FutureObj<'static, ()>, + ) -> Result<(), futures::task::SpawnError> { + self.0.spawn(obj); + Ok(()) + } +} diff --git a/ui/prompt.go b/ui/prompt.go deleted file mode 100644 index 8791f74..0000000 --- a/ui/prompt.go +++ /dev/null @@ -1,308 +0,0 @@ -package ui - -import ( - "errors" - "fmt" - "reflect" - "sort" - "strings" - - "github.com/manifoldco/promptui" - "github.com/railwayapp/cli/entity" -) - -type Prompt string -type Selection string - -const ( - InitNew Selection = "Empty Project" - InitFromTemplate Selection = "Starter Template" -) - -func PromptInit() (Selection, error) { - _, selection, err := selectString("Starting Point", []string{string(InitNew), string(InitFromTemplate)}) - return Selection(selection), err -} - -func PromptText(text string) (string, error) { - prompt := promptui.Prompt{ - Label: text, - } - return prompt.Run() -} - -func hasTeams(projects []*entity.Project) bool { - teamKeys := make(map[string]bool) - teams := make([]string, 0) - - for _, project := range projects { - if project.Team != nil { - if _, value := teamKeys[*project.Team]; !value { - teamKeys[*project.Team] = true - teams = append(teams, *project.Team) - } - } - } - - return len(teams) > 1 -} - -func promptTeams(projects []*entity.Project) (*string, error) { - if hasTeams(projects) { - teams := make([]string, 0) - teamCheck := make(map[string]bool) - for _, project := range projects { - if project.Team == nil { - continue - } - - // Ensure teams are only appended once by checking teamCheck - if _, hasSeenTeam := teamCheck[*project.Team]; !hasSeenTeam { - teams = append(teams, *project.Team) - teamCheck[*project.Team] = true - } - } - - _, team, err := selectString("Team", teams) - return &team, err - } - - return nil, nil -} - -func PromptProjects(projects []*entity.Project) (*entity.Project, error) { - // Check if need to prompt teams - team, err := promptTeams(projects) - if err != nil { - return nil, err - } - filteredProjects := make([]*entity.Project, 0) - - if team == nil { - filteredProjects = projects - } else { - for _, project := range projects { - if *project.Team == *team { - filteredProjects = append(filteredProjects, project) - } - } - } - - sort.Slice(filteredProjects, func(i int, j int) bool { - return filteredProjects[i].UpdatedAt > filteredProjects[j].UpdatedAt - }) - - i, _, err := selectCustom("Project", filteredProjects, func(index int) string { - return filteredProjects[index].Name - }) - return filteredProjects[i], err -} - -// PromptStarterTemplates prompts the user to select one of the provided starter templates -func PromptStarterTemplates(starters []*entity.Starter) (*entity.Starter, error) { - i, _, err := selectCustom("Starter", starters, func(index int) string { - return starters[index].Title - }) - - return starters[i], err -} - -func PromptIsRepoPrivate() (bool, error) { - _, visibility, err := selectString("Visibility", []string{"Public", "Private"}) - return visibility == "Private", err -} - -func PromptEnvVars(envVars []*entity.StarterEnvVar) (map[string]string, error) { - variables := make(map[string]string) - if len(envVars) > 0 { - fmt.Printf("\n%s\n", Bold("Environment Variables")) - } - - for _, envVar := range envVars { - prompt := promptui.Prompt{ - Label: envVar.Name, - Default: envVar.Default, - } - if envVar.Optional { - fmt.Printf("\n%s %s\n", envVar.Desc, GrayText("(Optional)")) - } else { - fmt.Printf("\n%s %s\n", envVar.Desc, GrayText("(Required)")) - prompt.Validate = validatorRequired("value required") - } - - v, err := prompt.Run() - if err != nil { - return nil, err - } - - variables[envVar.Name] = v - } - - // Extra newline to match the ones outputted in the loop - fmt.Print("\n") - - return variables, nil -} - -func PromptProjectName() (string, error) { - prompt := promptui.Prompt{ - Label: "Enter project name", - Templates: &promptui.PromptTemplates{ - Prompt: "{{ . }} ", - Valid: fmt.Sprintf("%s {{ . | bold }}: ", promptui.IconGood), - Invalid: fmt.Sprintf("%s {{ . | bold }}: ", promptui.IconBad), - Success: fmt.Sprintf("%s {{ . | magenta | bold }}: ", promptui.IconGood), - }, - Validate: validatorRequired("project name required"), - } - return prompt.Run() -} - -func PromptConfirmProjectName() (string, error) { - prompt := promptui.Prompt{ - Label: "Confirm project name", - Templates: &promptui.PromptTemplates{ - Prompt: "{{ . }} ", - Valid: fmt.Sprintf("%s {{ . | bold }}: ", promptui.IconGood), - Invalid: fmt.Sprintf("%s {{ . | bold }}: ", promptui.IconBad), - Success: fmt.Sprintf("%s {{ . | magenta | bold }}: ", promptui.IconGood), - }, - } - return prompt.Run() -} - -// PromptGitHubScopes prompts the user to select one of the provides scopes -func PromptGitHubScopes(scopes []string) (string, error) { - if len(scopes) == 1 { - return scopes[0], nil - } - - _, scope, err := selectString("GitHub Owner", scopes) - return scope, err -} - -func PromptEnvironments(environments []*entity.Environment) (*entity.Environment, error) { - if len(environments) == 1 { - environment := environments[0] - fmt.Printf("%s Environment: %s\n", promptui.IconGood, BlueText(environment.Name)) - return environment, nil - } - i, _, err := selectCustom("Environment", environments, func(index int) string { - return environments[index].Name - }) - if err != nil { - return nil, err - } - - return environments[i], nil -} - -func PromptServices(services []*entity.Service) (*entity.Service, error) { - if len(services) == 0 { - return &entity.Service{}, nil - } - if len(services) == 1 { - return services[0], nil - } - i, _, err := selectCustom("Service", services, func(index int) string { - return services[index].Name - }) - if err != nil { - return nil, err - } - - return services[i], nil -} - -func PromptPlugins(plugins []string) (string, error) { - i, _, err := selectString("Plugin", plugins) - return plugins[i], err -} - -// PromptYesNo prompts the user to continue an action using the common (y/N) action -func PromptYesNo(msg string) (bool, error) { - fmt.Printf("%s (y/N): ", msg) - var response string - _, err := fmt.Scan(&response) - if err != nil { - return false, err - } - response = strings.ToLower(response) - - isNo := response == "n" || response == "no" - isYes := response == "y" || response == "yes" - - if isYes { - return true, nil - } else if isNo { - return false, nil - } else { - fmt.Println("Please type yes or no and then press enter:") - return PromptYesNo(msg) - } -} - -func validatorRequired(errorMsg string) func(s string) error { - return func(s string) error { - if strings.TrimSpace(s) == "" { - return errors.New(errorMsg) - } - return nil - } -} - -// selectWrapper wraps an arbitrary stringify function + associated index, used by the select -// helpers so it can accept an arbitrary slice. It also implements the Stringer interface so -// it can automatically be printed by %s -type selectItemWrapper struct { - stringify func(index int) string - index int -} - -// String adheres to the Stringer interface and returns the string representation from the -// stringify function -func (w selectItemWrapper) String() string { - return w.stringify(w.index) -} - -// selectString prompts the user to select a string from the provided slice -func selectString(label string, items []string) (int, string, error) { - return selectCustom(label, items, func(index int) string { - return fmt.Sprintf("%v", items[index]) - }) -} - -// selectCustom prompts the user to select an item from the provided slice. A stringify function is passed, which -// is responsible for returning a label for the item, when called. -func selectCustom(label string, items interface{}, stringify func(index int) string) (int, string, error) { - v := reflect.ValueOf(items) - if v.Kind() != reflect.Slice { - panic(fmt.Errorf("forEachValue: expected slice type, found %q", v.Kind().String())) - } - wrappedItems := make([]selectItemWrapper, 0) - for i := 0; i < v.Len(); i++ { - wrappedItems = append(wrappedItems, selectItemWrapper{ - stringify: stringify, - index: i, - }) - } - - options := &promptui.Select{ - Label: fmt.Sprintf("Select %s", label), - Items: wrappedItems, - Size: 10, - Templates: &promptui.SelectTemplates{ - Active: fmt.Sprintf(`%s {{ . | underline }}`, promptui.IconSelect), - Inactive: ` {{ . }}`, - Selected: fmt.Sprintf("%s %s: {{ . | magenta | bold }} ", promptui.IconGood, label), - }, - Searcher: func(input string, i int) bool { - return strings.Contains( - strings.ToLower(stringify(i)), - strings.ToLower(input), - ) - }, - } - - return options.Run() -} diff --git a/ui/spinner.go b/ui/spinner.go deleted file mode 100644 index 48939cf..0000000 --- a/ui/spinner.go +++ /dev/null @@ -1,59 +0,0 @@ -package ui - -import ( - "fmt" - "os" - "time" - - "github.com/briandowns/spinner" -) - -type TrainSpinner []string - -var ( - TrainEmojis TrainSpinner = []string{"🚝", "🚅", "🚄", "🚇", "🚞", "🚈", "🚉", "🚂", "🚃", "🚊", "🚋"} -) - -type SpinnerCfg struct { - // Message specifies the text label that appears while loading - Message string - // Tokens is a list of emoji to rotate through, during loading - Tokens []string - // Duration is the amount of delay between each spinner "frame" - Duration time.Duration -} - -var s = &spinner.Spinner{} - -func StartSpinner(cfg *SpinnerCfg) { - if !SupportsANSICodes() { - fmt.Println(cfg.Message) - return - } - - if cfg.Tokens == nil { - cfg.Tokens = TrainEmojis - } - if cfg.Duration.Microseconds() == 0 { - cfg.Duration = time.Duration(100) * time.Millisecond - } - s = spinner.New(cfg.Tokens, cfg.Duration) - s.Writer = os.Stdout - - if cfg.Message != "" { - s.Suffix = " " + cfg.Message - } - - s.Start() -} - -func StopSpinner(msg string) { - if msg != "" { - s.FinalMSG = msg + "\n" - } - - // NOTE: Running Stop() when not active triggers a nil pointer - if s.Active() { - s.Stop() - } -} diff --git a/ui/text.go b/ui/text.go deleted file mode 100644 index 5e480d0..0000000 --- a/ui/text.go +++ /dev/null @@ -1,218 +0,0 @@ -package ui - -import ( - "fmt" - "math" - "sort" - "strings" - - _aurora "github.com/logrusorgru/aurora" -) - -var aurora _aurora.Aurora - -func init() { - // Disable colors automatically if no TTY detected - enableColors := SupportsANSICodes() - aurora = _aurora.NewAurora(enableColors) -} - -func Bold(payload string) _aurora.Value { - return aurora.Bold(payload) -} - -func RedText(payload string) _aurora.Value { - return aurora.Red(payload) -} - -func MagentaText(payload string) _aurora.Value { - return aurora.Magenta(payload) -} - -func BlueText(payload string) _aurora.Value { - return aurora.Blue(payload) -} - -func GrayText(payload string) _aurora.Value { - return aurora.Gray(10, payload) -} - -func LightGrayText(payload string) _aurora.Value { - return aurora.Gray(14, payload) -} - -func GreenText(payload string) _aurora.Value { - return aurora.Green(payload) -} - -func YellowText(payload string) _aurora.Value { - return aurora.Yellow(payload) -} - -func Heading(text string) string { - return _aurora.Sprintf(_aurora.Bold("==> %s\n").Magenta(), Bold(text)) -} - -func AlertDanger(text string) string { - return _aurora.Sprintf(_aurora.Bold("🚨 %s\n").Red(), text) -} - -func AlertWarning(text string) string { - return _aurora.Sprintf(_aurora.Bold("⚠️ %s\n").Yellow(), text) -} - -func AlertInfo(text string) string { - return _aurora.Sprintf(GrayText(Bold("💁 %s\n").String()), text) -} - -func VerboseInfo(isVerbose bool, text string) string { - if isVerbose { - return _aurora.Sprintf(MagentaText("💁 %s\n"), text) - } else { - return "" - } -} - -func Truncate(text string, maxLength int) string { - ellipsis := "..." - overflow := len(text) + len(ellipsis) - maxLength - if overflow < 0 { - return text - } - - visibleSize := float64(len(text)-overflow) / 2 - if visibleSize < 1 { - visibleSize = 1 - } - - // Account for visibleSize not being whole number - prefixLen := int(math.Ceil(visibleSize)) - suffixLen := int(math.Floor(visibleSize)) - - prefix := text[:prefixLen] - suffix := text[len(text)-suffixLen:] - - return prefix + ellipsis + suffix -} - -func ObscureText(text string) string { - return strings.Repeat("*", len(text)) -} - -func UnorderedList(items []string) string { - text := "" - for _, item := range items { - text += fmt.Sprintf("%s %s\n", Bold(LightGrayText("-").String()), item) - } - return text -} - -func OrderedList(items []string) string { - text := "" - for i, item := range items { - index := Bold(LightGrayText(fmt.Sprintf("%d)", i+1)).String()) - text += fmt.Sprintf("%s %s\n", index, item) - } - return text -} - -// Indent adds two space characters to the start of every line in the text -func Indent(text string) string { - return PrefixLines(text, " ") -} - -// Paragraph automatically wraps text (by word) to 60 chars -func Paragraph(text string) string { - const maxLineLength = 60 - - lines := make([]string, 0) - currentLine := "" - for _, word := range strings.Split(text, " ") { - currentLineWithNextWord := currentLine - if currentLineWithNextWord == "" { - currentLineWithNextWord += word - } else { - currentLineWithNextWord += " " + word - } - - if len(currentLineWithNextWord) > maxLineLength { - lines = append(lines, currentLine) - currentLine = word - } else { - currentLine = currentLineWithNextWord - } - } - - // Add unfinished line if there was one - if currentLine != "" { - lines = append(lines, currentLine) - } - - return strings.Join(lines, "\n") + "\n" -} - -// BlockQuote adds two space characters to the start of every line in the text -func BlockQuote(text string) string { - wrapped := strings.TrimSpace(Paragraph(text)) - return PrefixLines(wrapped, GrayText("> ").String()) -} - -// PrefixLines adds a string to the start of every line in the text -func PrefixLines(text, prefix string) string { - newText := "" - for _, line := range strings.Split(text, "\n") { - newText += fmt.Sprintf("%s%s\n", prefix, line) - } - return newText -} - -func KeyValues(items map[string]string) string { - type pair struct { - Key string - Value string - } - - // Need to move them into slice because maps have random order - pairs := make([]pair, 0) - - var maxKeyLengthWithPadding = 50 - var longestKey = 0 - - // Add pairs to slice and find longest key - for k, v := range items { - pairs = append(pairs, pair{Key: k, Value: v}) - if len(k) > longestKey { - longestKey = len(k) - } - } - - text := "" - - // order the pairs by key - sort.Slice(pairs, func(i, j int) bool { - return strings.Compare(pairs[j].Key, pairs[i].Key) > 0 - }) - - nameLength := min(longestKey, maxKeyLengthWithPadding) - for _, pair := range pairs { - prettyKey := fmt.Sprintf("%s:", GreenText(pair.Key)) - padding := strings.Repeat(" ", max(0, nameLength-len(pair.Key))) - text += fmt.Sprintf("%s%s %s\n", prettyKey, padding, pair.Value) - } - - return text -} - -func max(x, y int) int { - if x > y { - return x - } - return y -} - -func min(x, y int) int { - if x < y { - return x - } - return y -} diff --git a/ui/text_test.go b/ui/text_test.go deleted file mode 100644 index 9c36a57..0000000 --- a/ui/text_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package ui_test - -import ( - "github.com/railwayapp/cli/ui" - "github.com/stretchr/testify/require" - "strings" - "testing" -) - -var keyValuesTest = []struct { - name string - in map[string]string - out string -}{ - { - name: "Nil map should print nothing", - in: nil, - out: "", - }, - { - name: "Empty map should print nothing", - in: map[string]string{}, - out: "", - }, - { - name: "Output should always be alphabetical", - in: map[string]string{ - "zzz": "ZZZ", - "BBB": "bbb", - "AAA": "aaa", - "aaa": "AAA", - }, - out: multiline( - "AAA: aaa", - "BBB: bbb", - "aaa: AAA", - "zzz: ZZZ", - ), - }, - { - name: "Varying lengths should align values", - in: map[string]string{ - "A": "aaa", - "BB": "bbbbbbbb", - "CCC": "b", - }, - out: multiline( - "A: aaa", - "BB: bbbbbbbb", - "CCC: b", - ), - }, - { - name: "Super long keys should only pad others so much", - in: map[string]string{ - "A": "aaa", - "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB": "bbbbbbbb", - "CCC": "b", - }, - out: multiline( - "A: aaa", - "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB: bbbbbbbb", - "CCC: b", - ), - }, -} - -var unorderedListTest = []struct { - name string - in []string - out string -}{ - { - name: "Nil slice should print nothing", - in: nil, - out: "", - }, - { - name: "Empty slice should print nothing", - in: []string{}, - out: "", - }, - { - name: "Generic output should work", - in: []string{ - "Foo", - "Bar", - "Baz", - }, - out: multiline( - "- Foo", - "- Bar", - "- Baz", - ), - }, -} - -var orderedListTest = []struct { - name string - in []string - out string -}{ - { - name: "Nil slice should print nothing", - in: nil, - out: "", - }, - { - name: "Empty slice should print nothing", - in: []string{}, - out: "", - }, - { - name: "Generic output should work", - in: []string{ - "Foo", - "Bar", - "Baz", - }, - out: multiline( - "1) Foo", - "2) Bar", - "3) Baz", - ), - }, -} - -var truncateTest = []struct { - name string - inStr string - inLen int - out string -}{ - { - name: "Negative numbers work", - inStr: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - inLen: -10, - out: "0...Z", - }, - { - name: "Small length works", - inStr: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - inLen: 1, - out: "0...Z", - }, - { - name: "Odd length works", - inStr: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - inLen: 10, - out: "0123...XYZ", - }, - { - name: "Even length works", - inStr: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - inLen: 11, - out: "0123...WXYZ", - }, - { - name: "Full length works", - inStr: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - inLen: 100, - out: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - }, -} - -var paragraphTest = []struct { - name string - in string - out string -}{ - { - name: "Should wrap long lines", - in: "This is a long sentence, which is really boring, and demonstrates the wrapping functionality nicely. It should be about three lines.", - out: "This is a long sentence, which is really boring, and\ndemonstrates the wrapping functionality nicely. It should be\nabout three lines.\n", - }, - { - name: "Should wrap precisely", - in: "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a", - out: "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\na a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\na a a a a a a a a a a a a a a a a a a a a a a a a a a a\n", - }, -} - -var prefixTest = []struct { - name string - inStr string - inPrefix string - out string -}{ - { - name: "Should prefix lines", - inStr: "Line 1\nLine 2\nLine 3", - inPrefix: "> ", - out: multiline( - "> Line 1", - "> Line 2", - "> Line 3", - ), - }, -} - -func TestKeyValues(t *testing.T) { - for _, tt := range keyValuesTest { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.out, ui.KeyValues(tt.in)) - }) - } -} - -func TestUnorderedList(t *testing.T) { - for _, tt := range unorderedListTest { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.out, ui.UnorderedList(tt.in)) - }) - } -} - -func TestOrderedList(t *testing.T) { - for _, tt := range orderedListTest { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.out, ui.OrderedList(tt.in)) - }) - } -} - -func TestTruncate(t *testing.T) { - for _, tt := range truncateTest { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.out, ui.Truncate(tt.inStr, tt.inLen)) - }) - } -} - -func TestParagraph(t *testing.T) { - for _, tt := range paragraphTest { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.out, ui.Paragraph(tt.in)) - }) - } -} - -func TestPrefixLines(t *testing.T) { - for _, tt := range prefixTest { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.out, ui.PrefixLines(tt.inStr, tt.inPrefix)) - }) - } -} - -// multiline provides a human-readable way to create a multiline block of text -func multiline(lines ...string) string { - return strings.Join(lines, "\n") + "\n" -} diff --git a/ui/tty.go b/ui/tty.go deleted file mode 100644 index 2b27a67..0000000 --- a/ui/tty.go +++ /dev/null @@ -1,10 +0,0 @@ -package ui - -import ( - "github.com/mattn/go-isatty" - "os" -) - -func SupportsANSICodes() bool { - return isatty.IsTerminal(os.Stdout.Fd()) -} diff --git a/uninstall.sh b/uninstall.sh deleted file mode 100644 index d609969..0000000 --- a/uninstall.sh +++ /dev/null @@ -1,3 +0,0 @@ -rm $(which railway) - -echo "Uninstalled Railway" diff --git a/uuid/main.go b/uuid/main.go deleted file mode 100644 index a727d5c..0000000 --- a/uuid/main.go +++ /dev/null @@ -1,11 +0,0 @@ -package uuid - -import ( - "github.com/google/uuid" -) - -func IsValidUUID(id string) bool { - _, err := uuid.Parse(id) - - return err == nil -}