├── .gitignore ├── .goreleaser.yaml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── assets ├── eleven_cli.png ├── eleven_logo_black.png ├── eleven_logo_white.png └── vscode.png ├── go.mod ├── go.sum ├── install.ps1 ├── install.sh ├── internal ├── agent │ ├── check_domain_reachability.go │ ├── client.go │ ├── client_builder.go │ ├── init_instance.go │ ├── install_runtimes.go │ └── reconcile_served_ports_state.go ├── cloudproviders │ ├── aws │ │ ├── errors_presenter.go │ │ ├── errors_presenter_test.go │ │ ├── user_config_local_resolver.go │ │ └── user_config_local_resolver_test.go │ └── hetzner │ │ ├── errors_presenter.go │ │ ├── errors_presenter_test.go │ │ ├── user_config_local_resolver.go │ │ └── user_config_local_resolver_test.go ├── cmd │ ├── aws.go │ ├── cloud_providers.go │ ├── cloud_providers_test.go │ ├── edit.go │ ├── hetzner.go │ ├── init.go │ ├── login.go │ ├── remove.go │ ├── root.go │ ├── serve.go │ ├── uninstall.go │ └── unserve.go ├── config │ ├── colors.go │ ├── github.go │ ├── github_prod.go │ └── user_config.go ├── dependencies │ ├── aws_edit.go │ ├── aws_init.go │ ├── aws_remove.go │ ├── aws_serve.go │ ├── aws_shared.go │ ├── aws_uninstall.go │ ├── aws_unserve.go │ ├── entities.go │ ├── hetzner_edit.go │ ├── hetzner_init.go │ ├── hetzner_remove.go │ ├── hetzner_serve.go │ ├── hetzner_shared.go │ ├── hetzner_uninstall.go │ ├── hetzner_unserve.go │ ├── hooks.go │ ├── login.go │ ├── shared.go │ └── wire_gen.go ├── entities │ ├── env.go │ ├── env_repositories.go │ ├── env_repositories_test.go │ ├── env_served_ports.go │ └── env_served_ports_test.go ├── exceptions │ └── cmd.go ├── features │ ├── edit.go │ ├── init.go │ ├── login.go │ ├── remove.go │ ├── serve.go │ ├── uninstall.go │ └── unserve.go ├── globals │ └── values.go ├── hooks │ ├── domain_reachability_checker.go │ └── pre_remove.go ├── interfaces │ ├── browser.go │ ├── github.go │ ├── logger.go │ ├── sleeper.go │ ├── ssh.go │ ├── user_config.go │ └── vscode.go ├── mocks │ ├── aws_user_config_env_vars_resolver.go │ ├── aws_user_config_files_resolver.go │ ├── hetzner_user_config_env_vars_resolver.go │ ├── hetzner_user_config_files_resolver.go │ ├── presenters_init.go │ └── views_displayer.go ├── presenters │ ├── edit.go │ ├── errors.go │ ├── init.go │ ├── login.go │ ├── remove.go │ ├── serve.go │ ├── uninstall.go │ └── unserve.go ├── ssh │ ├── config.go │ ├── config_add_host_test.go │ ├── config_remove_host_test.go │ ├── config_update_host_test.go │ ├── keys.go │ ├── keys_add_pem_test.go │ ├── keys_remove_pem_test.go │ ├── known_hosts.go │ ├── known_hosts_add_test.go │ ├── known_hosts_remove_test.go │ ├── port_forwarding.go │ └── testdata │ │ ├── empty_known_hosts │ │ ├── empty_ssh_config │ │ ├── non_empty_known_hosts │ │ └── non_empty_ssh_config ├── stepper │ ├── step.go │ └── stepper.go ├── system │ ├── browser.go │ ├── cli.go │ ├── displayer.go │ ├── env_vars.go │ ├── logger.go │ ├── new_line.go │ ├── paths.go │ ├── paths_test.go │ └── sleeper.go ├── tools.go ├── views │ ├── base.go │ ├── edit.go │ ├── errors.go │ ├── init.go │ ├── login.go │ ├── remove.go │ ├── serve.go │ ├── uninstall.go │ └── unserve.go └── vscode │ ├── cli.go │ ├── extensions.go │ └── process.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | out/ 3 | vendor/ 4 | *.out 5 | dist/ 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | builds: 4 | - 5 | tags: 6 | - prod 7 | env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - 386 15 | - amd64 16 | - arm 17 | - arm64 18 | binary: eleven 19 | checksum: 20 | name_template: 'checksums.txt' 21 | snapshot: 22 | name_template: "{{ incpatch .Version }}-next" 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "main.go", 13 | "args": [ 14 | "hetzner", 15 | "--region", 16 | "fsn1", 17 | "init", 18 | "IBM-Cloud/ruby-rails-helloworld", 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Jeremy Levy jje.levy@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/eleven_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-sh/cli/47872ad01d9dd7032da356c2d91b256a231f0d1b/assets/eleven_cli.png -------------------------------------------------------------------------------- /assets/eleven_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-sh/cli/47872ad01d9dd7032da356c2d91b256a231f0d1b/assets/eleven_logo_black.png -------------------------------------------------------------------------------- /assets/eleven_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-sh/cli/47872ad01d9dd7032da356c2d91b256a231f0d1b/assets/eleven_logo_white.png -------------------------------------------------------------------------------- /assets/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-sh/cli/47872ad01d9dd7032da356c2d91b256a231f0d1b/assets/vscode.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eleven-sh/cli 2 | 3 | go 1.18 4 | 5 | replace github.com/eleven-sh/aws-cloud-provider v0.0.0 => ../aws-cloud-provider 6 | 7 | replace github.com/eleven-sh/hetzner-cloud-provider v0.0.0 => ../hetzner-cloud-provider 8 | 9 | replace github.com/eleven-sh/eleven v0.0.0 => ../eleven 10 | 11 | replace github.com/eleven-sh/agent v0.0.0 => ../agent 12 | 13 | require ( 14 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d 15 | github.com/aws/aws-sdk-go-v2/config v1.13.1 16 | github.com/briandowns/spinner v1.19.0 17 | github.com/eleven-sh/agent v0.0.0 18 | github.com/eleven-sh/aws-cloud-provider v0.0.0 19 | github.com/eleven-sh/eleven v0.0.0 20 | github.com/eleven-sh/hetzner-cloud-provider v0.0.0 21 | github.com/golang/mock v1.6.0 22 | github.com/google/go-github/v43 v43.0.0 23 | github.com/google/uuid v1.3.0 24 | github.com/google/wire v0.5.0 25 | github.com/jwalton/gchalk v1.3.0 26 | github.com/kevinburke/ssh_config v1.1.0 27 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 28 | github.com/spf13/cobra v1.3.0 29 | github.com/spf13/pflag v1.0.5 30 | github.com/spf13/viper v1.10.1 31 | golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000 32 | golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 33 | google.golang.org/grpc v1.49.0 34 | ) 35 | 36 | require ( 37 | github.com/aws/aws-sdk-go-v2 v1.15.0 // indirect 38 | github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect 39 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.6.0 // indirect 40 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect 41 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect 42 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 // indirect 43 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 // indirect 44 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.13.0 // indirect 45 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.11.0 // indirect 46 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.29.0 // indirect 47 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0 // indirect 48 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.5.0 // indirect 49 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 // indirect 50 | github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 // indirect 51 | github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 // indirect 52 | github.com/aws/smithy-go v1.11.1 // indirect 53 | github.com/beorn7/perks v1.0.1 // indirect 54 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 55 | github.com/fatih/color v1.13.0 // indirect 56 | github.com/fsnotify/fsnotify v1.5.1 // indirect 57 | github.com/golang/protobuf v1.5.2 // indirect 58 | github.com/google/go-querystring v1.1.0 // indirect 59 | github.com/google/subcommands v1.0.1 // indirect 60 | github.com/gosimple/slug v1.12.0 // indirect 61 | github.com/gosimple/unidecode v1.0.1 // indirect 62 | github.com/hashicorp/hcl v1.0.0 // indirect 63 | github.com/hetznercloud/hcloud-go v1.35.2 // indirect 64 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 65 | github.com/jmespath/go-jmespath v0.4.0 // indirect 66 | github.com/jwalton/go-supportscolor v1.1.0 // indirect 67 | github.com/kr/text v0.2.0 // indirect 68 | github.com/magiconair/properties v1.8.5 // indirect 69 | github.com/mattn/go-colorable v0.1.12 // indirect 70 | github.com/mattn/go-isatty v0.0.14 // indirect 71 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 72 | github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect 73 | github.com/mitchellh/mapstructure v1.4.3 // indirect 74 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 75 | github.com/pelletier/go-toml v1.9.5 // indirect 76 | github.com/pmezard/go-difflib v1.0.0 // indirect 77 | github.com/prometheus/client_golang v1.12.1 // indirect 78 | github.com/prometheus/client_model v0.2.0 // indirect 79 | github.com/prometheus/common v0.32.1 // indirect 80 | github.com/prometheus/procfs v0.8.0 // indirect 81 | github.com/spf13/afero v1.6.0 // indirect 82 | github.com/spf13/cast v1.4.1 // indirect 83 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 84 | github.com/subosito/gotenv v1.2.0 // indirect 85 | github.com/whilp/git-urls v1.0.0 // indirect 86 | golang.org/x/mod v0.5.0 // indirect 87 | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect 88 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 89 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 90 | golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect 91 | golang.org/x/tools v0.1.7 // indirect 92 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 93 | google.golang.org/appengine v1.6.7 // indirect 94 | google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect 95 | google.golang.org/protobuf v1.28.1 // indirect 96 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 97 | gopkg.in/ini.v1 v1.66.2 // indirect 98 | gopkg.in/yaml.v2 v2.4.0 // indirect 99 | ) 100 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | # Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | # TODO(everyone): Keep this script simple and easily auditable. 4 | 5 | $ErrorActionPreference = 'Stop' 6 | 7 | if ($v) { 8 | $Version = "v${v}" 9 | } 10 | if ($Args.Length -eq 1) { 11 | $Version = $Args.Get(0) 12 | } 13 | 14 | $Releases = "https://api.github.com/repos/eleven-sh/cli/releases" 15 | $LatestVersion = (Invoke-WebRequest $Releases | ConvertFrom-Json)[0].tag_name 16 | 17 | $Arch = $env:PROCESSOR_ARCHITECTURE 18 | 19 | $ElevenInstall = $env:ELEVEN_INSTALL 20 | $BinDir = if ($ElevenInstall) { 21 | "${ElevenInstall}\bin" 22 | } else { 23 | "${Home}\.eleven\bin" 24 | } 25 | 26 | $ElevenTar = "${BinDir}\eleven.tar.gz" 27 | $ElevenExe = "${BinDir}\eleven.exe" 28 | 29 | $DownloadUrl = if (!$Version) { 30 | "https://github.com/eleven-sh/cli/releases/latest/download/cli_$($LatestVersion.substring(1))_windows_$($Arch.ToLower()).tar.gz" 31 | } else { 32 | "https://github.com/eleven-sh/cli/releases/download/${Version}/cli_$($Version.substring(1))_windows_$($Arch.ToLower()).tar.gz" 33 | } 34 | 35 | if (!(Test-Path $BinDir)) { 36 | New-Item $BinDir -ItemType Directory | Out-Null 37 | } 38 | 39 | curl.exe -Lo $ElevenTar $DownloadUrl 40 | 41 | tar.exe xf $ElevenTar -C $BinDir 42 | 43 | Remove-Item $ElevenTar 44 | 45 | $User = [System.EnvironmentVariableTarget]::User 46 | $Path = [System.Environment]::GetEnvironmentVariable('Path', $User) 47 | if (!(";${Path};".ToLower() -like "*;${BinDir};*".ToLower())) { 48 | [System.Environment]::SetEnvironmentVariable('Path', "${Path};${BinDir}", $User) 49 | $Env:Path += ";${BinDir}" 50 | } 51 | 52 | Write-Output "Eleven was installed successfully to ${ElevenExe}" 53 | Write-Output "Run 'eleven --help' to get started" 54 | Write-Output "Stuck? Open a new issue at https://github.com/eleven-sh/cli/issues/new" -------------------------------------------------------------------------------- /internal/agent/check_domain_reachability.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eleven-sh/agent/proto" 7 | ) 8 | 9 | type CheckDomainReachabilityStream interface { 10 | Recv() (*proto.CheckDomainReachabilityReply, error) 11 | } 12 | 13 | func (c Client) CheckDomainReachability( 14 | checkDomainReachabilityRequest *proto.CheckDomainReachabilityRequest, 15 | streamHandler func(stream CheckDomainReachabilityStream) error, 16 | ) error { 17 | 18 | return c.Execute(func(agentGRPCClient proto.AgentClient) error { 19 | checkDomainReachabilityStream, err := agentGRPCClient.CheckDomainReachability( 20 | context.TODO(), 21 | checkDomainReachabilityRequest, 22 | ) 23 | 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return streamHandler(checkDomainReachabilityStream) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /internal/agent/client.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/eleven-sh/agent/proto" 7 | "github.com/eleven-sh/cli/internal/ssh" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/credentials/insecure" 10 | ) 11 | 12 | type ClientInterface interface { 13 | InitInstance( 14 | initInstanceRequest *proto.InitInstanceRequest, 15 | streamHandler func(stream InitInstanceStream) error, 16 | ) error 17 | 18 | InstallRuntimes( 19 | installRuntimesRequest *proto.InstallRuntimesRequest, 20 | streamHandler func(stream InstallRuntimesStream) error, 21 | ) error 22 | 23 | ReconcileServedPortsState( 24 | serveRequest *proto.ReconcileServedPortsStateRequest, 25 | streamHandler func(stream ReconcileServedPortsStateStream) error, 26 | ) error 27 | 28 | CheckDomainReachability( 29 | checkDomainReachabilityRequest *proto.CheckDomainReachabilityRequest, 30 | streamHandler func(stream CheckDomainReachabilityStream) error, 31 | ) error 32 | } 33 | 34 | type ClientConfig struct { 35 | ServerRootUser string 36 | ServerSSHPrivateKeyBytes []byte 37 | ServerAddr string 38 | LocalAddr string 39 | RemoteAddrProtocol string 40 | RemoteAddr string 41 | } 42 | 43 | type Client struct { 44 | config ClientConfig 45 | } 46 | 47 | func NewClient(config ClientConfig) Client { 48 | return Client{ 49 | config: config, 50 | } 51 | } 52 | 53 | func (c Client) Execute(fnToExec func(agentGRPCClient proto.AgentClient) error) error { 54 | pollTimeoutChan := time.After(48 * time.Second) 55 | pollSleepDuration := 4 * time.Second 56 | 57 | var portForwarderReadyResp ssh.PortForwarderReadyResp 58 | var portForwarderRespChan chan error 59 | 60 | for { 61 | select { 62 | case <-pollTimeoutChan: 63 | return portForwarderReadyResp.Error 64 | default: 65 | portForwarderRespChan = make(chan error, 1) 66 | portForwarderReadyChan := make(chan ssh.PortForwarderReadyResp) 67 | portForwarder := ssh.NewPortForwarder() 68 | 69 | // Open an SSH tunnel to the GRPC server 70 | // from "localAddr" to "remoteAddr" inside server ("serverAddr") 71 | go func() { 72 | portForwarderRespChan <- portForwarder.Forward( 73 | portForwarderReadyChan, 74 | c.config.ServerSSHPrivateKeyBytes, 75 | c.config.ServerRootUser, 76 | c.config.ServerAddr, 77 | c.config.LocalAddr, 78 | c.config.RemoteAddrProtocol, 79 | c.config.RemoteAddr, 80 | ) 81 | }() 82 | 83 | portForwarderReadyResp = <-portForwarderReadyChan 84 | } 85 | 86 | if portForwarderReadyResp.Error == nil { 87 | break 88 | } 89 | 90 | time.Sleep(pollSleepDuration) 91 | } 92 | 93 | grpcRespChan := make(chan error, 1) 94 | 95 | go func() { 96 | grpcConn, err := grpc.Dial( 97 | portForwarderReadyResp.LocalAddr, 98 | grpc.WithTransportCredentials( 99 | insecure.NewCredentials(), 100 | ), 101 | ) 102 | 103 | if err != nil { 104 | grpcRespChan <- err 105 | return 106 | } 107 | 108 | defer grpcConn.Close() 109 | 110 | agentGRPCClient := proto.NewAgentClient(grpcConn) 111 | 112 | grpcRespChan <- fnToExec(agentGRPCClient) 113 | }() 114 | 115 | select { 116 | case portForwarderErr := <-portForwarderRespChan: 117 | return portForwarderErr 118 | case grpcErr := <-grpcRespChan: 119 | return grpcErr 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /internal/agent/client_builder.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/eleven-sh/agent/config" 7 | ) 8 | 9 | type ClientBuilder interface { 10 | Build(config ClientConfig) ClientInterface 11 | } 12 | 13 | type DefaultClientBuilder struct{} 14 | 15 | func NewDefaultClientBuilder() DefaultClientBuilder { 16 | return DefaultClientBuilder{} 17 | } 18 | 19 | func (DefaultClientBuilder) Build(config ClientConfig) ClientInterface { 20 | return NewClient(config) 21 | } 22 | 23 | func NewDefaultClientConfig( 24 | sshPrivateKeyBytes []byte, 25 | instancePublicIPAddress string, 26 | ) ClientConfig { 27 | 28 | return ClientConfig{ 29 | ServerRootUser: config.ElevenUserName, 30 | ServerSSHPrivateKeyBytes: sshPrivateKeyBytes, 31 | ServerAddr: net.JoinHostPort( 32 | instancePublicIPAddress, 33 | config.SSHServerListenPort, 34 | ), 35 | // Ends with ":" to let "net.listener" 36 | // choose a random available port for us 37 | LocalAddr: "127.0.0.1:", 38 | RemoteAddrProtocol: config.GRPCServerAddrProtocol, 39 | RemoteAddr: config.GRPCServerAddr, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/agent/init_instance.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eleven-sh/agent/proto" 7 | ) 8 | 9 | type InitInstanceStream interface { 10 | Recv() (*proto.InitInstanceReply, error) 11 | } 12 | 13 | func (c Client) InitInstance( 14 | initInstanceRequest *proto.InitInstanceRequest, 15 | streamHandler func(stream InitInstanceStream) error, 16 | ) error { 17 | 18 | return c.Execute(func(agentGRPCClient proto.AgentClient) error { 19 | initInstanceStream, err := agentGRPCClient.InitInstance( 20 | context.TODO(), 21 | initInstanceRequest, 22 | ) 23 | 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return streamHandler(initInstanceStream) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /internal/agent/install_runtimes.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eleven-sh/agent/proto" 7 | ) 8 | 9 | type InstallRuntimesStream interface { 10 | Recv() (*proto.InstallRuntimesReply, error) 11 | } 12 | 13 | func (c Client) InstallRuntimes( 14 | installRuntimesRequest *proto.InstallRuntimesRequest, 15 | streamHandler func(stream InstallRuntimesStream) error, 16 | ) error { 17 | 18 | return c.Execute(func(agentGRPCClient proto.AgentClient) error { 19 | installRuntimesStream, err := agentGRPCClient.InstallRuntimes( 20 | context.TODO(), 21 | installRuntimesRequest, 22 | ) 23 | 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return streamHandler(installRuntimesStream) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /internal/agent/reconcile_served_ports_state.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eleven-sh/agent/proto" 7 | ) 8 | 9 | type ReconcileServedPortsStateStream interface { 10 | Recv() (*proto.ReconcileServedPortsStateReply, error) 11 | } 12 | 13 | func (c Client) ReconcileServedPortsState( 14 | serveRequest *proto.ReconcileServedPortsStateRequest, 15 | streamHandler func(stream ReconcileServedPortsStateStream) error, 16 | ) error { 17 | 18 | return c.Execute(func(agentGRPCClient proto.AgentClient) error { 19 | serveStream, err := agentGRPCClient.ReconcileServedPortsState( 20 | context.TODO(), 21 | serveRequest, 22 | ) 23 | 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return streamHandler(serveStream) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /internal/cloudproviders/aws/errors_presenter.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/eleven-sh/aws-cloud-provider/config" 8 | "github.com/eleven-sh/aws-cloud-provider/service" 9 | "github.com/eleven-sh/aws-cloud-provider/userconfig" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/entities" 13 | ) 14 | 15 | type AWSViewableErrorBuilder struct { 16 | presenters.ElevenViewableErrorBuilder 17 | } 18 | 19 | func NewAWSViewableErrorBuilder() AWSViewableErrorBuilder { 20 | return AWSViewableErrorBuilder{} 21 | } 22 | 23 | func (a AWSViewableErrorBuilder) Build(err error) (viewableError *views.ViewableError) { 24 | viewableError = &views.ViewableError{} 25 | 26 | if errors.Is(err, entities.ErrElevenNotInstalled) { 27 | viewableError.Title = "Eleven not installed" 28 | viewableError.Message = "Eleven is not installed in this region on this AWS account.\n\n" + 29 | "Please double check the passed credentials and region." 30 | 31 | return 32 | } 33 | 34 | if errors.Is(err, entities.ErrUninstallExistingEnvs) { 35 | viewableError.Title = "Existing sandboxes" 36 | viewableError.Message = "All sandboxes need to be removed before uninstalling Eleven." 37 | 38 | return 39 | } 40 | 41 | if errors.Is(err, userconfig.ErrMissingConfig) { 42 | viewableError.Title = "No AWS account found" 43 | viewableError.Message = fmt.Sprintf(`An AWS account can be configured: 44 | 45 | - by setting the "%s", "%s" and "%s" environment variables. 46 | 47 | - by installing the AWS CLI and running "aws configure".`, 48 | userconfig.AWSAccessKeyIDEnvVar, 49 | userconfig.AWSSecretAccessKeyEnvVar, 50 | userconfig.AWSRegionEnvVar, 51 | ) 52 | 53 | return 54 | } 55 | 56 | if errors.Is(err, userconfig.ErrMissingAccessKeyInEnv) { 57 | viewableError.Title = "Missing environment variable" 58 | viewableError.Message = fmt.Sprintf( 59 | "The environment variable \"%s\" needs to be set.", 60 | userconfig.AWSAccessKeyIDEnvVar, 61 | ) 62 | 63 | return 64 | } 65 | 66 | if errors.Is(err, userconfig.ErrMissingSecretInEnv) { 67 | viewableError.Title = "Missing environment variable" 68 | viewableError.Message = fmt.Sprintf( 69 | "The environment variable \"%s\" needs to be set.", 70 | userconfig.AWSSecretAccessKeyEnvVar, 71 | ) 72 | 73 | return 74 | } 75 | 76 | if errors.Is(err, userconfig.ErrMissingRegionInEnv) || 77 | errors.Is(err, userconfig.ErrMissingRegionInFiles) { 78 | 79 | viewableError.Title = "Missing region" 80 | viewableError.Message = fmt.Sprintf( 81 | "A region needs to be specified by setting the \"%s\" environment variable or by using the \"--region\" flag.", 82 | userconfig.AWSRegionEnvVar, 83 | ) 84 | 85 | return 86 | } 87 | 88 | if typedError, ok := err.(userconfig.ErrProfileNotFound); ok { 89 | viewableError.Title = "Configuration profile not found" 90 | viewableError.Message = fmt.Sprintf( 91 | "The profile \"%s\" was not found in your AWS configuration.\n\n(Searched in \"%s\" and \"%s\").", 92 | typedError.Profile, 93 | typedError.CredentialsFilePath, 94 | typedError.ConfigFilePath, 95 | ) 96 | 97 | return 98 | } 99 | 100 | if typedError, ok := err.(config.ErrInvalidRegion); ok { 101 | viewableError.Title = "Invalid region" 102 | viewableError.Message = fmt.Sprintf( 103 | "The region \"%s\" is invalid.", 104 | typedError.Region, 105 | ) 106 | 107 | return 108 | } 109 | 110 | if typedError, ok := err.(config.ErrInvalidAccessKeyID); ok { 111 | viewableError.Title = "Invalid access key ID" 112 | viewableError.Message = fmt.Sprintf( 113 | "The access key ID \"%s\" is invalid.", 114 | typedError.AccessKeyID, 115 | ) 116 | 117 | return 118 | } 119 | 120 | if typedError, ok := err.(config.ErrInvalidSecretAccessKey); ok { 121 | viewableError.Title = "Invalid secret access key" 122 | viewableError.Message = fmt.Sprintf( 123 | "The secret access key \"%s\" is invalid.", 124 | typedError.SecretAccessKey, 125 | ) 126 | 127 | return 128 | } 129 | 130 | if typedError, ok := err.(service.ErrInvalidInstanceType); ok { 131 | viewableError.Title = "Invalid instance type" 132 | viewableError.Message = fmt.Sprintf( 133 | "The instance type \"%s\" is invalid in the region \"%s\".", 134 | typedError.InstanceType, 135 | typedError.Region, 136 | ) 137 | 138 | return 139 | } 140 | 141 | if typedError, ok := err.(service.ErrInvalidInstanceTypeArch); ok { 142 | viewableError.Title = "Unsupported instance type" 143 | viewableError.Message = fmt.Sprintf( 144 | "The instance type \"%s\" is not supported by Eleven.\n\n"+ 145 | "Only on-demand linux instances with EBS and \"%s\" architectures are supported.", 146 | typedError.InstanceType, 147 | typedError.SupportedArchs, 148 | ) 149 | 150 | return 151 | } 152 | 153 | viewableError = a.ElevenViewableErrorBuilder.Build(err) 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /internal/cloudproviders/aws/errors_presenter_test.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/eleven-sh/aws-cloud-provider/userconfig" 8 | "github.com/eleven-sh/eleven/entities" 9 | ) 10 | 11 | func TestViewableErrorBuilder(t *testing.T) { 12 | testCases := []struct { 13 | test string 14 | passedError error 15 | expectedViewableErrorTitle string 16 | }{ 17 | { 18 | test: "with unknown error", 19 | passedError: errors.New(""), 20 | expectedViewableErrorTitle: "Unknown error", 21 | }, 22 | 23 | { 24 | test: "with Eleven error", 25 | passedError: entities.ErrEditCreatingEnv{}, 26 | expectedViewableErrorTitle: "Invalid sandbox state", 27 | }, 28 | 29 | { 30 | test: "with AWS error", 31 | passedError: userconfig.ErrMissingAccessKeyInEnv, 32 | expectedViewableErrorTitle: "Missing environment variable", 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | t.Run(tc.test, func(t *testing.T) { 38 | builder := NewAWSViewableErrorBuilder() 39 | 40 | err := builder.Build(tc.passedError) 41 | 42 | if err.Title != tc.expectedViewableErrorTitle { 43 | t.Fatalf( 44 | "expected viewable error title to equal '%s', got '%s'", 45 | tc.expectedViewableErrorTitle, 46 | err.Title, 47 | ) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/cloudproviders/aws/user_config_local_resolver.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/eleven-sh/aws-cloud-provider/userconfig" 7 | ) 8 | 9 | type UserConfigLocalResolverOpts struct { 10 | Profile string 11 | } 12 | 13 | //go:generate go run github.com/golang/mock/mockgen -destination=../../mocks/aws_user_config_env_vars_resolver.go -package=mocks -mock_names UserConfigEnvVarsResolver=AWSUserConfigEnvVarsResolver github.com/eleven-sh/cli/internal/cloudproviders/aws UserConfigEnvVarsResolver 14 | type UserConfigEnvVarsResolver interface { 15 | Resolve() (*userconfig.Config, error) 16 | } 17 | 18 | //go:generate go run github.com/golang/mock/mockgen -destination=../../mocks/aws_user_config_files_resolver.go -package=mocks -mock_names UserConfigFilesResolver=AWSUserConfigFilesResolver github.com/eleven-sh/cli/internal/cloudproviders/aws UserConfigFilesResolver 19 | type UserConfigFilesResolver interface { 20 | Resolve() (*userconfig.Config, error) 21 | } 22 | 23 | // UserConfigLocalResolver represents the default implementation 24 | // of the UserConfigResolver interface, used by most AWS commands via 25 | // the SDKConfigStaticBuilder. 26 | // 27 | // It retrieves the AWS account configuration from environment variables 28 | // (via the UserConfigLocalEnvVarsResolver interface) and fallback to config 29 | // files (via the UserConfigLocalFilesResolver interface) otherwise. 30 | // 31 | type UserConfigLocalResolver struct { 32 | envVarsResolver UserConfigEnvVarsResolver 33 | configFilesResolver UserConfigFilesResolver 34 | opts UserConfigLocalResolverOpts 35 | } 36 | 37 | // NewUserConfigLocalResolver constructs 38 | // the UserConfigLocalResolver struct. 39 | // Used by Wire in dependencies. 40 | // 41 | func NewUserConfigLocalResolver( 42 | envVarsResolver UserConfigEnvVarsResolver, 43 | configFilesResolver UserConfigFilesResolver, 44 | opts UserConfigLocalResolverOpts, 45 | ) UserConfigLocalResolver { 46 | 47 | return UserConfigLocalResolver{ 48 | envVarsResolver: envVarsResolver, 49 | configFilesResolver: configFilesResolver, 50 | opts: opts, 51 | } 52 | } 53 | 54 | // Resolve retrieves the AWS account configuration from environment variables 55 | // and fallback to config files if no environment variables were found. 56 | // 57 | // If the Profile option is set, environment variables are ignored 58 | // and the profile is directly loaded from config files. 59 | // 60 | func (u UserConfigLocalResolver) Resolve() (*userconfig.Config, error) { 61 | var userConfig *userconfig.Config 62 | var err error 63 | 64 | if len(u.opts.Profile) == 0 { 65 | userConfig, err = u.envVarsResolver.Resolve() 66 | 67 | if err != nil && !errors.Is(err, userconfig.ErrMissingConfig) { 68 | return nil, err 69 | } 70 | } 71 | 72 | if userConfig == nil { 73 | userConfig, err = u.configFilesResolver.Resolve() 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | } 79 | 80 | return userConfig, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/cloudproviders/hetzner/errors_presenter.go: -------------------------------------------------------------------------------- 1 | package hetzner 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/eleven-sh/cli/internal/presenters" 8 | "github.com/eleven-sh/cli/internal/views" 9 | "github.com/eleven-sh/eleven/entities" 10 | "github.com/eleven-sh/hetzner-cloud-provider/config" 11 | "github.com/eleven-sh/hetzner-cloud-provider/service" 12 | "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 13 | ) 14 | 15 | type HetznerViewableErrorBuilder struct { 16 | presenters.ElevenViewableErrorBuilder 17 | } 18 | 19 | func NewHetznerViewableErrorBuilder() HetznerViewableErrorBuilder { 20 | return HetznerViewableErrorBuilder{} 21 | } 22 | 23 | func (h HetznerViewableErrorBuilder) Build(err error) (viewableError *views.ViewableError) { 24 | viewableError = &views.ViewableError{} 25 | 26 | if errors.Is(err, entities.ErrElevenNotInstalled) { 27 | viewableError.Title = "Eleven not installed" 28 | viewableError.Message = "Eleven is not installed in this region on this Hetzner account.\n\n" + 29 | "Please double check the passed API token and region." 30 | 31 | return 32 | } 33 | 34 | if errors.Is(err, entities.ErrUninstallExistingEnvs) { 35 | viewableError.Title = "Existing sandboxes" 36 | viewableError.Message = "All sandboxes need to be removed before uninstalling Eleven." 37 | 38 | return 39 | } 40 | 41 | if errors.Is(err, userconfig.ErrMissingConfig) { 42 | viewableError.Title = "No Hetzner account found" 43 | viewableError.Message = fmt.Sprintf(`An Hetzner account can be configured: 44 | 45 | - by setting the "%s" and "%s" environment variables. 46 | 47 | - by installing the Hetzner CLI and running "hcloud context create ".`, 48 | userconfig.HetznerAPITokenEnvVar, 49 | userconfig.HetznerRegionEnvVar, 50 | ) 51 | 52 | return 53 | } 54 | 55 | if errors.Is(err, userconfig.ErrMissingRegionInEnv) || 56 | errors.Is(err, userconfig.ErrMissingRegion) { 57 | 58 | viewableError.Title = "Missing region" 59 | viewableError.Message = fmt.Sprintf( 60 | "A region needs to be specified by setting the \"%s\" environment variable or by using the \"--region\" flag.", 61 | userconfig.HetznerRegionEnvVar, 62 | ) 63 | 64 | return 65 | } 66 | 67 | if typedError, ok := err.(config.ErrContextNotFound); ok { 68 | viewableError.Title = "Configuration context not found" 69 | viewableError.Message = fmt.Sprintf( 70 | "The context \"%s\" was not found in your Hetzner configuration.\n\n(Searched in \"%s\").", 71 | typedError.Context, 72 | typedError.ConfigFilePath, 73 | ) 74 | 75 | return 76 | } 77 | 78 | if typedError, ok := err.(config.ErrInvalidRegion); ok { 79 | viewableError.Title = "Invalid region" 80 | viewableError.Message = fmt.Sprintf( 81 | "The region \"%s\" is invalid.", 82 | typedError.Region, 83 | ) 84 | 85 | return 86 | } 87 | 88 | if typedError, ok := err.(config.ErrInvalidAPIToken); ok { 89 | viewableError.Title = "Invalid API token" 90 | viewableError.Message = fmt.Sprintf( 91 | "The API token \"%s\" is invalid.", 92 | typedError.APIToken, 93 | ) 94 | 95 | return 96 | } 97 | 98 | if typedError, ok := err.(service.ErrInvalidInstanceType); ok { 99 | viewableError.Title = "Invalid instance type" 100 | viewableError.Message = fmt.Sprintf( 101 | "The instance type \"%s\" is invalid in the region \"%s\".", 102 | typedError.InstanceType, 103 | typedError.Region, 104 | ) 105 | 106 | return 107 | } 108 | 109 | viewableError = h.ElevenViewableErrorBuilder.Build(err) 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /internal/cloudproviders/hetzner/errors_presenter_test.go: -------------------------------------------------------------------------------- 1 | package hetzner 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/eleven-sh/eleven/entities" 8 | "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 9 | ) 10 | 11 | func TestViewableErrorBuilder(t *testing.T) { 12 | testCases := []struct { 13 | test string 14 | passedError error 15 | expectedViewableErrorTitle string 16 | }{ 17 | { 18 | test: "with unknown error", 19 | passedError: errors.New(""), 20 | expectedViewableErrorTitle: "Unknown error", 21 | }, 22 | 23 | { 24 | test: "with Eleven error", 25 | passedError: entities.ErrEditCreatingEnv{}, 26 | expectedViewableErrorTitle: "Invalid sandbox state", 27 | }, 28 | 29 | { 30 | test: "with Hetzner error", 31 | passedError: userconfig.ErrMissingRegionInEnv, 32 | expectedViewableErrorTitle: "Missing region", 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | t.Run(tc.test, func(t *testing.T) { 38 | builder := NewHetznerViewableErrorBuilder() 39 | 40 | err := builder.Build(tc.passedError) 41 | 42 | if err.Title != tc.expectedViewableErrorTitle { 43 | t.Fatalf( 44 | "expected viewable error title to equal '%s', got '%s'", 45 | tc.expectedViewableErrorTitle, 46 | err.Title, 47 | ) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/cloudproviders/hetzner/user_config_local_resolver.go: -------------------------------------------------------------------------------- 1 | package hetzner 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 7 | ) 8 | 9 | type UserConfigLocalResolverOpts struct { 10 | Context string 11 | } 12 | 13 | //go:generate go run github.com/golang/mock/mockgen -destination=../../mocks/hetzner_user_config_env_vars_resolver.go -package=mocks -mock_names UserConfigEnvVarsResolver=HetznerUserConfigEnvVarsResolver github.com/eleven-sh/cli/internal/cloudproviders/hetzner UserConfigEnvVarsResolver 14 | type UserConfigEnvVarsResolver interface { 15 | Resolve() (*userconfig.Config, error) 16 | } 17 | 18 | //go:generate go run github.com/golang/mock/mockgen -destination=../../mocks/hetzner_user_config_files_resolver.go -package=mocks -mock_names UserConfigFilesResolver=HetznerUserConfigFilesResolver github.com/eleven-sh/cli/internal/cloudproviders/hetzner UserConfigFilesResolver 19 | type UserConfigFilesResolver interface { 20 | Resolve() (*userconfig.Config, error) 21 | } 22 | 23 | // UserConfigLocalResolver represents the default implementation 24 | // of the UserConfigResolver interface, used by most Hetzner commands via 25 | // the SDKConfigStaticBuilder. 26 | // 27 | // It retrieves the Hetzner account configuration from environment variables 28 | // (via the UserConfigLocalEnvVarsResolver interface) and fallback to config 29 | // files (via the UserConfigLocalFilesResolver interface) otherwise. 30 | // 31 | type UserConfigLocalResolver struct { 32 | envVarsResolver UserConfigEnvVarsResolver 33 | configFilesResolver UserConfigFilesResolver 34 | opts UserConfigLocalResolverOpts 35 | } 36 | 37 | // NewUserConfigLocalResolver constructs 38 | // the UserConfigLocalResolver struct. 39 | // Used by Wire in dependencies. 40 | // 41 | func NewUserConfigLocalResolver( 42 | envVarsResolver UserConfigEnvVarsResolver, 43 | configFilesResolver UserConfigFilesResolver, 44 | opts UserConfigLocalResolverOpts, 45 | ) UserConfigLocalResolver { 46 | 47 | return UserConfigLocalResolver{ 48 | envVarsResolver: envVarsResolver, 49 | configFilesResolver: configFilesResolver, 50 | opts: opts, 51 | } 52 | } 53 | 54 | // Resolve retrieves the Hetzner account configuration from environment variables 55 | // and fallback to config files if no environment variables were found. 56 | // 57 | // If the Profile option is set, environment variables are ignored 58 | // and the profile is directly loaded from config files. 59 | // 60 | func (u UserConfigLocalResolver) Resolve() (*userconfig.Config, error) { 61 | var userConfig *userconfig.Config 62 | var err error 63 | 64 | if len(u.opts.Context) == 0 { 65 | userConfig, err = u.envVarsResolver.Resolve() 66 | 67 | if err != nil && !errors.Is(err, userconfig.ErrMissingConfig) { 68 | return nil, err 69 | } 70 | } 71 | 72 | if userConfig == nil { 73 | userConfig, err = u.configFilesResolver.Resolve() 74 | 75 | if err != nil { 76 | return nil, err 77 | } 78 | } 79 | 80 | return userConfig, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/cloudproviders/hetzner/user_config_local_resolver_test.go: -------------------------------------------------------------------------------- 1 | package hetzner 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/eleven-sh/cli/internal/mocks" 8 | "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 9 | "github.com/golang/mock/gomock" 10 | ) 11 | 12 | func TestUserConfigLocalResolving(t *testing.T) { 13 | testCases := []struct { 14 | test string 15 | configInEnvVars *userconfig.Config 16 | errorDuringEnvVarsResolving error 17 | configInFiles *userconfig.Config 18 | errorDuringConfigFilesResolving error 19 | contextOpts string 20 | expectedConfig *userconfig.Config 21 | expectedError error 22 | }{ 23 | { 24 | test: "no env vars, no config files", 25 | errorDuringEnvVarsResolving: userconfig.ErrMissingConfig, 26 | errorDuringConfigFilesResolving: userconfig.ErrMissingConfig, 27 | expectedConfig: nil, 28 | expectedError: userconfig.ErrMissingConfig, 29 | }, 30 | 31 | { 32 | test: "only env vars", 33 | configInEnvVars: userconfig.NewConfig("a", "b"), 34 | errorDuringConfigFilesResolving: userconfig.ErrMissingConfig, 35 | expectedConfig: userconfig.NewConfig("a", "b"), 36 | expectedError: nil, 37 | }, 38 | 39 | { 40 | test: "only config files", 41 | errorDuringEnvVarsResolving: userconfig.ErrMissingConfig, 42 | configInFiles: userconfig.NewConfig("a", "b"), 43 | expectedConfig: userconfig.NewConfig("a", "b"), 44 | expectedError: nil, 45 | }, 46 | 47 | { 48 | test: "env vars and config files", 49 | configInEnvVars: userconfig.NewConfig("a", "b"), 50 | configInFiles: userconfig.NewConfig("c", "d"), 51 | expectedConfig: userconfig.NewConfig("a", "b"), 52 | expectedError: nil, 53 | }, 54 | 55 | { 56 | test: "env vars, config files and context", 57 | configInEnvVars: userconfig.NewConfig("a", "b"), 58 | configInFiles: userconfig.NewConfig("c", "d"), 59 | contextOpts: "production", 60 | expectedConfig: userconfig.NewConfig("c", "d"), 61 | expectedError: nil, 62 | }, 63 | 64 | { 65 | test: "env vars and errored config files", 66 | configInEnvVars: userconfig.NewConfig("a", "b"), 67 | errorDuringConfigFilesResolving: userconfig.ErrMissingRegion, 68 | expectedConfig: userconfig.NewConfig("a", "b"), 69 | expectedError: nil, 70 | }, 71 | 72 | { 73 | test: "env vars, errored config files and context", 74 | configInEnvVars: userconfig.NewConfig("a", "b"), 75 | errorDuringConfigFilesResolving: userconfig.ErrMissingRegion, 76 | contextOpts: "production", 77 | expectedConfig: nil, 78 | expectedError: userconfig.ErrMissingRegion, 79 | }, 80 | } 81 | 82 | for _, tc := range testCases { 83 | t.Run(tc.test, func(t *testing.T) { 84 | mockCtrl := gomock.NewController(t) 85 | defer mockCtrl.Finish() 86 | 87 | userConfigEnvVarsResolverMock := mocks.NewHetznerUserConfigEnvVarsResolver(mockCtrl) 88 | userConfigEnvVarsResolverMock.EXPECT().Resolve().Return( 89 | tc.configInEnvVars, 90 | tc.errorDuringEnvVarsResolving, 91 | ).AnyTimes() 92 | 93 | userConfigFilesResolverMock := mocks.NewHetznerUserConfigFilesResolver(mockCtrl) 94 | userConfigFilesResolverMock.EXPECT().Resolve().Return( 95 | tc.configInFiles, 96 | tc.errorDuringConfigFilesResolving, 97 | ).AnyTimes() 98 | 99 | resolver := NewUserConfigLocalResolver( 100 | userConfigEnvVarsResolverMock, 101 | userConfigFilesResolverMock, 102 | UserConfigLocalResolverOpts{ 103 | Context: tc.contextOpts, 104 | }, 105 | ) 106 | 107 | resolvedConfig, err := resolver.Resolve() 108 | 109 | if tc.expectedError == nil && err != nil { 110 | t.Fatalf("expected no error, got '%+v'", err) 111 | } 112 | 113 | if tc.expectedError != nil && !errors.Is(err, tc.expectedError) { 114 | t.Fatalf("expected error to equal '%+v', got '%+v'", tc.expectedError, err) 115 | } 116 | 117 | if tc.expectedConfig != nil && *resolvedConfig != *tc.expectedConfig { 118 | t.Fatalf("expected config to equal '%+v', got '%+v'", *tc.expectedConfig, *resolvedConfig) 119 | } 120 | 121 | if tc.expectedConfig == nil && resolvedConfig != nil { 122 | t.Fatalf("expected no config, got '%+v'", *resolvedConfig) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /internal/cmd/aws.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/config" 5 | "github.com/eleven-sh/cli/internal/dependencies" 6 | "github.com/eleven-sh/cli/internal/globals" 7 | "github.com/eleven-sh/eleven/features" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func getAWSCloudProvider() *cloudProvider { 12 | var profile string 13 | var region string 14 | 15 | var credentialsFilePath string 16 | var configFilePath string 17 | 18 | var aws = cloudProvider{ 19 | ShortName: "aws", 20 | LongName: "Amazon Web Services", 21 | GlobalName: globals.AWSCloudProvider, 22 | 23 | DefaultInstanceType: "t2.medium", 24 | ExampleInstanceType: "m4.large", 25 | 26 | AddFlagsToBaseCmd: func(baseCmd *cobra.Command) { 27 | baseCmd.Flags().StringVar( 28 | &profile, 29 | "profile", 30 | "", 31 | "the configuration profile to use to access your AWS account", 32 | ) 33 | 34 | baseCmd.Flags().StringVar( 35 | ®ion, 36 | "region", 37 | "", 38 | "the region to use to access your AWS account", 39 | ) 40 | 41 | credentialsFilePath = config.DefaultSharedCredentialsFilename() 42 | configFilePath = config.DefaultSharedConfigFilename() 43 | }, 44 | 45 | ProvideInitFeature: func() cloudProviderFeature[features.InitInput] { 46 | return dependencies.ProvideAWSInitFeature( 47 | region, 48 | profile, 49 | credentialsFilePath, 50 | configFilePath, 51 | ) 52 | }, 53 | 54 | ProvideEditFeature: func() cloudProviderFeature[features.EditInput] { 55 | return dependencies.ProvideAWSEditFeature( 56 | region, 57 | profile, 58 | credentialsFilePath, 59 | configFilePath, 60 | ) 61 | }, 62 | 63 | ProvideRemoveFeature: func() cloudProviderFeature[features.RemoveInput] { 64 | return dependencies.ProvideAWSRemoveFeature( 65 | region, 66 | profile, 67 | credentialsFilePath, 68 | configFilePath, 69 | ) 70 | }, 71 | 72 | ProvideServeFeature: func() cloudProviderFeature[features.ServeInput] { 73 | return dependencies.ProvideAWSServeFeature( 74 | region, 75 | profile, 76 | credentialsFilePath, 77 | configFilePath, 78 | ) 79 | }, 80 | 81 | ProvideUnserveFeature: func() cloudProviderFeature[features.UnserveInput] { 82 | return dependencies.ProvideAWSUnserveFeature( 83 | region, 84 | profile, 85 | credentialsFilePath, 86 | configFilePath, 87 | ) 88 | }, 89 | 90 | UninstallSuccessMessage: "Eleven has been uninstalled from this region on this AWS account.", 91 | UninstallAlreadyUninstalledMessage: "Eleven is already uninstalled in this region on this AWS account.", 92 | 93 | ProvideUninstallFeature: func() cloudProviderFeature[features.UninstallInput] { 94 | return dependencies.ProvideAWSUninstallFeature( 95 | region, 96 | profile, 97 | credentialsFilePath, 98 | configFilePath, 99 | ) 100 | }, 101 | } 102 | 103 | return &aws 104 | } 105 | -------------------------------------------------------------------------------- /internal/cmd/cloud_providers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/globals" 5 | "github.com/eleven-sh/eleven/features" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | ) 9 | 10 | type cloudProviderFeature[T interface{}] interface { 11 | Execute(input T) error 12 | } 13 | 14 | type cloudProvider struct { 15 | LongName string 16 | ShortName string 17 | GlobalName globals.CloudProvider 18 | 19 | DefaultInstanceType string 20 | ExampleInstanceType string 21 | 22 | AddFlagsToBaseCmd func(*cobra.Command) 23 | 24 | ProvideInitFeature func() cloudProviderFeature[features.InitInput] 25 | ProvideEditFeature func() cloudProviderFeature[features.EditInput] 26 | ProvideRemoveFeature func() cloudProviderFeature[features.RemoveInput] 27 | ProvideServeFeature func() cloudProviderFeature[features.ServeInput] 28 | ProvideUnserveFeature func() cloudProviderFeature[features.UnserveInput] 29 | 30 | UninstallSuccessMessage string 31 | UninstallAlreadyUninstalledMessage string 32 | ProvideUninstallFeature func() cloudProviderFeature[features.UninstallInput] 33 | } 34 | 35 | func addCloudProviderCmdToRootCmd( 36 | rootCmd *cobra.Command, 37 | provider *cloudProvider, 38 | ) *cobra.Command { 39 | 40 | var cloudProviderCmd = &cobra.Command{ 41 | Use: provider.ShortName, 42 | 43 | Short: "Use Eleven on " + provider.LongName, 44 | 45 | Long: `Use Eleven on ` + provider.LongName + `. 46 | 47 | To begin, create your first sandbox using the command: 48 | 49 | eleven ` + provider.ShortName + ` init 50 | 51 | Once created, you may want to connect your editor to it using the command: 52 | 53 | eleven ` + provider.ShortName + ` edit 54 | 55 | If you don't plan to use this sandbox again, you could remove it using the command: 56 | 57 | eleven ` + provider.ShortName + ` remove `, 58 | 59 | Example: ` eleven ` + provider.ShortName + ` init eleven-api --instance-type ` + provider.ExampleInstanceType + ` 60 | eleven ` + provider.ShortName + ` edit eleven-api 61 | eleven ` + provider.ShortName + ` remove eleven-api`, 62 | 63 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 64 | ensureUserIsLoggedIn() 65 | populateCurrentCloudProviderGlobals(provider.GlobalName, cmd) 66 | }, 67 | } 68 | 69 | provider.AddFlagsToBaseCmd(cloudProviderCmd) 70 | 71 | rootCmd.AddCommand(cloudProviderCmd) 72 | 73 | return cloudProviderCmd 74 | } 75 | 76 | func populateCurrentCloudProviderGlobals( 77 | cloudProvider globals.CloudProvider, 78 | cloudProviderCmd *cobra.Command, 79 | ) { 80 | 81 | globals.CurrentCloudProvider = cloudProvider 82 | globals.CurrentCloudProviderArgs = "" 83 | 84 | // command ran without subcommand -> help displayed -> no args to parse 85 | if !cloudProviderCmd.HasParent() { 86 | return 87 | } 88 | 89 | cloudProviderCmd.Parent().Flags().VisitAll(func(f *pflag.Flag) { 90 | if !f.Changed { 91 | return 92 | } 93 | 94 | if len(globals.CurrentCloudProviderArgs) > 0 { 95 | globals.CurrentCloudProviderArgs += " " 96 | } 97 | 98 | globals.CurrentCloudProviderArgs += "--" + f.Name + " " + f.Value.String() 99 | }) 100 | } 101 | 102 | func init() { 103 | availableCloudProviders := []*cloudProvider{ 104 | getAWSCloudProvider(), 105 | getHetznerCloudProvider(), 106 | } 107 | 108 | for _, cloudProvider := range availableCloudProviders { 109 | cloudProviderCmd := addCloudProviderCmdToRootCmd(rootCmd, cloudProvider) 110 | 111 | loadInitCmd(cloudProviderCmd, cloudProvider) 112 | loadEditCmd(cloudProviderCmd, cloudProvider) 113 | 114 | loadServeCmd(cloudProviderCmd, cloudProvider) 115 | loadUnserveCmd(cloudProviderCmd, cloudProvider) 116 | 117 | loadRemoveCmd(cloudProviderCmd, cloudProvider) 118 | loadUninstallCmd(cloudProviderCmd, cloudProvider) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/cmd/cloud_providers_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eleven-sh/cli/internal/globals" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func TestPopulateCurrentCloudProviderGlobals(t *testing.T) { 11 | testCases := []struct { 12 | test string 13 | cloudProvider globals.CloudProvider 14 | baseCmd *cobra.Command 15 | baseCmdFlags map[string]string 16 | childrenCmd *cobra.Command 17 | expectedGlobalCloudProvider globals.CloudProvider 18 | expectedGlobalCloudProviderArgs string 19 | }{ 20 | { 21 | test: "with no base command", 22 | cloudProvider: globals.AWSCloudProvider, 23 | baseCmd: &cobra.Command{}, 24 | expectedGlobalCloudProvider: globals.AWSCloudProvider, 25 | expectedGlobalCloudProviderArgs: "", 26 | }, 27 | 28 | { 29 | test: "with base command without flags", 30 | cloudProvider: globals.HetznerCloudProvider, 31 | baseCmd: &cobra.Command{}, 32 | childrenCmd: &cobra.Command{}, 33 | expectedGlobalCloudProvider: globals.HetznerCloudProvider, 34 | expectedGlobalCloudProviderArgs: "", 35 | }, 36 | 37 | { 38 | test: "with base command and one flags", 39 | cloudProvider: globals.AWSCloudProvider, 40 | baseCmd: &cobra.Command{}, 41 | baseCmdFlags: map[string]string{ 42 | "flag1": "flag1_value", 43 | }, 44 | childrenCmd: &cobra.Command{}, 45 | expectedGlobalCloudProvider: globals.AWSCloudProvider, 46 | expectedGlobalCloudProviderArgs: "--flag1 flag1_value", 47 | }, 48 | 49 | { 50 | test: "with base command and three flags", 51 | cloudProvider: globals.AWSCloudProvider, 52 | baseCmd: &cobra.Command{}, 53 | baseCmdFlags: map[string]string{ 54 | "flag1": "flag1_value", 55 | "flag4": "flag4_value", 56 | // Changed set to "false". See below. 57 | "flag6": "", 58 | }, 59 | childrenCmd: &cobra.Command{}, 60 | expectedGlobalCloudProvider: globals.AWSCloudProvider, 61 | expectedGlobalCloudProviderArgs: "--flag1 flag1_value --flag4 flag4_value", 62 | }, 63 | } 64 | 65 | for _, tc := range testCases { 66 | t.Run(tc.test, func(t *testing.T) { 67 | cmdUnderTest := tc.baseCmd 68 | 69 | if tc.childrenCmd != nil { 70 | tc.baseCmd.AddCommand(tc.childrenCmd) 71 | cmdUnderTest = tc.childrenCmd 72 | } 73 | 74 | if len(tc.baseCmdFlags) > 0 { 75 | for flagName, flagValue := range tc.baseCmdFlags { 76 | tc.baseCmd.Flags().String(flagName, flagValue, "") 77 | 78 | if len(flagValue) == 0 { 79 | continue 80 | } 81 | 82 | tc.baseCmd.Flags().Lookup(flagName).Changed = true 83 | } 84 | } 85 | 86 | populateCurrentCloudProviderGlobals( 87 | tc.cloudProvider, 88 | cmdUnderTest, 89 | ) 90 | 91 | if tc.expectedGlobalCloudProvider != globals.CurrentCloudProvider { 92 | t.Fatalf( 93 | "expected global cloud provider to equal '%s', got '%s'", 94 | tc.expectedGlobalCloudProvider, 95 | globals.CurrentCloudProvider, 96 | ) 97 | } 98 | 99 | if tc.expectedGlobalCloudProviderArgs != globals.CurrentCloudProviderArgs { 100 | t.Fatalf( 101 | "expected global cloud provider args to equal '%s', got '%s'", 102 | tc.expectedGlobalCloudProviderArgs, 103 | globals.CurrentCloudProviderArgs, 104 | ) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/cmd/edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/eleven-sh/cli/internal/dependencies" 10 | "github.com/eleven-sh/cli/internal/exceptions" 11 | "github.com/eleven-sh/cli/internal/vscode" 12 | "github.com/eleven-sh/eleven/features" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func loadEditCmd( 17 | providerCmd *cobra.Command, 18 | provider *cloudProvider, 19 | ) { 20 | 21 | var editCmd = &cobra.Command{ 22 | Use: "edit ", 23 | 24 | Short: "Connect your editor to a sandbox", 25 | 26 | Long: `Connect your editor to a sandbox. 27 | 28 | This command connects your preferred editor to a sandbox. 29 | 30 | Supported editor(s): Microsoft Visual Studio Code`, 31 | 32 | Example: " eleven " + provider.ShortName + " edit eleven-api", 33 | 34 | Args: cobra.ExactArgs(1), 35 | 36 | Run: func(cmd *cobra.Command, args []string) { 37 | 38 | elevenViewableErrorBuilder := dependencies.ProvideElevenViewableErrorBuilder() 39 | baseView := dependencies.ProvideBaseView() 40 | 41 | missingRequirements := []string{} 42 | 43 | vscodeCLI := vscode.CLI{} 44 | _, err := vscodeCLI.LookupPath(runtime.GOOS) 45 | 46 | if vscodeCLINotFoundErr, ok := err.(vscode.ErrCLINotFound); ok { 47 | missingRequirements = append( 48 | missingRequirements, 49 | fmt.Sprintf( 50 | "Visual Studio Code (looked in \"%s)", 51 | strings.Join(vscodeCLINotFoundErr.VisitedPaths, "\", \"")+"\"", 52 | ), 53 | ) 54 | } 55 | 56 | if len(missingRequirements) > 0 { 57 | missingRequirementsErr := exceptions.ErrMissingRequirements{ 58 | MissingRequirements: missingRequirements, 59 | } 60 | 61 | baseView.ShowErrorViewWithStartingNewLine( 62 | elevenViewableErrorBuilder.Build( 63 | missingRequirementsErr, 64 | ), 65 | ) 66 | 67 | os.Exit(1) 68 | } 69 | 70 | envName := args[0] 71 | 72 | editInput := features.EditInput{ 73 | EnvName: envName, 74 | } 75 | 76 | edit := provider.ProvideEditFeature() 77 | 78 | if err := edit.Execute(editInput); err != nil { 79 | os.Exit(1) 80 | } 81 | }, 82 | } 83 | 84 | providerCmd.AddCommand(editCmd) 85 | } 86 | -------------------------------------------------------------------------------- /internal/cmd/hetzner.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/dependencies" 5 | "github.com/eleven-sh/cli/internal/globals" 6 | "github.com/eleven-sh/cli/internal/system" 7 | "github.com/eleven-sh/eleven/features" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func getHetznerCloudProvider() *cloudProvider { 12 | var context string 13 | var region string 14 | 15 | var hetzner = cloudProvider{ 16 | ShortName: "hetzner", 17 | LongName: "Hetzner", 18 | GlobalName: globals.HetznerCloudProvider, 19 | 20 | DefaultInstanceType: "cx11", 21 | ExampleInstanceType: "cx21", 22 | 23 | AddFlagsToBaseCmd: func(baseCmd *cobra.Command) { 24 | baseCmd.Flags().StringVar( 25 | &context, 26 | "context", 27 | "", 28 | "the configuration context to use to access your Hetzner account", 29 | ) 30 | 31 | baseCmd.Flags().StringVar( 32 | ®ion, 33 | "region", 34 | "", 35 | "the region to use to access your Hetzner account", 36 | ) 37 | }, 38 | 39 | ProvideInitFeature: func() cloudProviderFeature[features.InitInput] { 40 | return dependencies.ProvideHetznerInitFeature( 41 | system.UserConfigDir(), 42 | region, 43 | context, 44 | ) 45 | }, 46 | 47 | ProvideEditFeature: func() cloudProviderFeature[features.EditInput] { 48 | return dependencies.ProvideHetznerEditFeature( 49 | system.UserConfigDir(), 50 | region, 51 | context, 52 | ) 53 | }, 54 | 55 | ProvideRemoveFeature: func() cloudProviderFeature[features.RemoveInput] { 56 | return dependencies.ProvideHetznerRemoveFeature( 57 | system.UserConfigDir(), 58 | region, 59 | context, 60 | ) 61 | }, 62 | 63 | ProvideServeFeature: func() cloudProviderFeature[features.ServeInput] { 64 | return dependencies.ProvideHetznerServeFeature( 65 | system.UserConfigDir(), 66 | region, 67 | context, 68 | ) 69 | }, 70 | 71 | ProvideUnserveFeature: func() cloudProviderFeature[features.UnserveInput] { 72 | return dependencies.ProvideHetznerUnserveFeature( 73 | system.UserConfigDir(), 74 | region, 75 | context, 76 | ) 77 | }, 78 | 79 | UninstallSuccessMessage: "Eleven has been uninstalled from this region on this Hetzner account.", 80 | UninstallAlreadyUninstalledMessage: "Eleven is already uninstalled in this region on this Hetzner account.", 81 | 82 | ProvideUninstallFeature: func() cloudProviderFeature[features.UninstallInput] { 83 | return dependencies.ProvideHetznerUninstallFeature( 84 | system.UserConfigDir(), 85 | region, 86 | context, 87 | ) 88 | }, 89 | } 90 | 91 | return &hetzner 92 | } 93 | -------------------------------------------------------------------------------- /internal/cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/cli/internal/dependencies" 7 | "github.com/eleven-sh/eleven/entities" 8 | "github.com/eleven-sh/eleven/features" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func loadInitCmd( 13 | providerCmd *cobra.Command, 14 | provider *cloudProvider, 15 | ) { 16 | 17 | var instanceType string 18 | var runtimes []string 19 | var repositories []string 20 | 21 | var initCmd = &cobra.Command{ 22 | Use: "init ", 23 | 24 | Short: "Initialize a sandbox", 25 | 26 | Long: `Initialize a new sandbox. 27 | 28 | To choose the type of instance that will run the sandbox, use the "--instance-type" flag. 29 | 30 | To install some runtimes in the sandbox, use the "--runtimes" flag. 31 | 32 | To clone some GitHub repositories in the sandbox, use the "--repositories" flag.`, 33 | 34 | Example: ` eleven ` + provider.ShortName + ` init eleven-api 35 | eleven ` + provider.ShortName + ` init eleven-api --instance-type ` + provider.ExampleInstanceType + ` --runtimes node@18.7.0,docker --repositories repo,organization/repo`, 36 | 37 | Args: cobra.ExactArgs(1), 38 | 39 | Run: func(cmd *cobra.Command, args []string) { 40 | 41 | elevenViewableErrorBuilder := dependencies.ProvideElevenViewableErrorBuilder() 42 | baseView := dependencies.ProvideBaseView() 43 | 44 | checkForRepositoryExistence := true 45 | repositoryResolver := dependencies.ProvideEnvRepositoriesResolver() 46 | repositories, err := repositoryResolver.Resolve( 47 | repositories, 48 | checkForRepositoryExistence, 49 | ) 50 | 51 | if err != nil { 52 | baseView.ShowErrorViewWithStartingNewLine( 53 | elevenViewableErrorBuilder.Build( 54 | err, 55 | ), 56 | ) 57 | 58 | os.Exit(1) 59 | } 60 | 61 | envName := args[0] 62 | 63 | sshConfig := dependencies.ProvideSSHConfigManager() 64 | sshCfgDupHostCt, err := sshConfig.CountEntriesWithHostPrefix( 65 | entities.BuildInitialLocalSSHCfgHostnameForEnv(envName), 66 | ) 67 | 68 | if err != nil { 69 | baseView.ShowErrorViewWithStartingNewLine( 70 | elevenViewableErrorBuilder.Build( 71 | err, 72 | ), 73 | ) 74 | 75 | os.Exit(1) 76 | } 77 | 78 | initInput := features.InitInput{ 79 | InstanceType: instanceType, 80 | EnvName: envName, 81 | LocalSSHCfgDupHostCt: sshCfgDupHostCt, 82 | Repositories: repositories, 83 | Runtimes: runtimes, 84 | } 85 | 86 | init := provider.ProvideInitFeature() 87 | 88 | if err := init.Execute(initInput); err != nil { 89 | os.Exit(1) 90 | } 91 | }, 92 | } 93 | 94 | initCmd.Flags().StringVar( 95 | &instanceType, 96 | "instance-type", 97 | provider.DefaultInstanceType, 98 | "the type of instance that will run the sandbox", 99 | ) 100 | 101 | initCmd.Flags().StringSliceVar( 102 | &runtimes, 103 | "runtimes", 104 | []string{}, 105 | "the runtimes to install in the sandbox", 106 | ) 107 | 108 | initCmd.Flags().StringSliceVar( 109 | &repositories, 110 | "repositories", 111 | []string{}, 112 | "the repositories to clone in the sandbox", 113 | ) 114 | 115 | providerCmd.AddCommand(initCmd) 116 | } 117 | -------------------------------------------------------------------------------- /internal/cmd/login.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/cli/internal/dependencies" 7 | "github.com/eleven-sh/cli/internal/features" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // loginCmd represents the "eleven login" command 12 | var loginCmd = &cobra.Command{ 13 | Use: "login", 14 | 15 | Short: "Connect a GitHub account", 16 | 17 | Long: `Connect a GitHub account. 18 | 19 | This command connects a GitHub account to use with Eleven. 20 | 21 | Eleven requires the following permissions: 22 | 23 | - "Public SSH keys" and "Repositories" to let you access your repositories from your sandboxes 24 | 25 | - "Personal user data" to configure Git 26 | 27 | All your data (including the OAuth access token) will only be stored locally.`, 28 | 29 | Example: " eleven login", 30 | 31 | Run: func(cmd *cobra.Command, args []string) { 32 | loginInput := features.LoginInput{} 33 | 34 | login := dependencies.ProvideLoginFeature() 35 | 36 | if err := login.Execute(loginInput); err != nil { 37 | os.Exit(1) 38 | } 39 | }, 40 | } 41 | 42 | func init() { 43 | rootCmd.AddCommand(loginCmd) 44 | } 45 | -------------------------------------------------------------------------------- /internal/cmd/remove.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/cli/internal/dependencies" 7 | "github.com/eleven-sh/cli/internal/system" 8 | "github.com/eleven-sh/eleven/features" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func loadRemoveCmd( 13 | providerCmd *cobra.Command, 14 | provider *cloudProvider, 15 | ) { 16 | 17 | var forceRemove bool 18 | 19 | var removeCmd = &cobra.Command{ 20 | Use: "remove ", 21 | 22 | Short: "Remove a sandbox", 23 | 24 | Long: `Remove an existing sandbox. 25 | 26 | The sandbox will be PERMANENTLY removed along with ALL your un-pushed work. 27 | 28 | There is no going back, so please be sure before running this command.`, 29 | 30 | Example: ` eleven ` + provider.ShortName + ` remove eleven-api 31 | eleven ` + provider.ShortName + ` remove eleven-api --force`, 32 | 33 | Args: cobra.ExactArgs(1), 34 | 35 | Run: func(cmd *cobra.Command, args []string) { 36 | envName := args[0] 37 | 38 | removeInput := features.RemoveInput{ 39 | EnvName: envName, 40 | PreRemoveHook: dependencies.ProvidePreRemoveHook(), 41 | ForceRemove: forceRemove, 42 | ConfirmRemove: func() (bool, error) { 43 | logger := system.NewLogger() 44 | return system.AskForConfirmation( 45 | logger, 46 | os.Stdin, 47 | "All your un-pushed work will be lost.", 48 | ) 49 | }, 50 | } 51 | 52 | remove := provider.ProvideRemoveFeature() 53 | 54 | if err := remove.Execute(removeInput); err != nil { 55 | os.Exit(1) 56 | } 57 | }, 58 | } 59 | 60 | removeCmd.Flags().BoolVar( 61 | &forceRemove, 62 | "force", 63 | false, 64 | "remove without confirmation", 65 | ) 66 | 67 | providerCmd.AddCommand(removeCmd) 68 | } 69 | -------------------------------------------------------------------------------- /internal/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/eleven-sh/cli/internal/config" 10 | "github.com/eleven-sh/cli/internal/dependencies" 11 | "github.com/eleven-sh/cli/internal/exceptions" 12 | "github.com/eleven-sh/cli/internal/system" 13 | "github.com/eleven-sh/eleven/github" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | // rootCmd represents the base command when called without any subcommands 19 | var rootCmd = &cobra.Command{ 20 | Use: "eleven", 21 | 22 | Short: "Code sandboxes running in your cloud provider account", 23 | 24 | Long: `Eleven - Code sandboxes running in your cloud provider account 25 | 26 | To begin, run the command "eleven login" to connect your GitHub account. 27 | 28 | From there, the most common workflow is: 29 | 30 | - eleven init : to initialize a new sandbox 31 | 32 | - eleven edit : to connect your preferred editor to a sandbox 33 | 34 | - eleven remove : to remove an existing sandbox`, 35 | 36 | TraverseChildren: true, 37 | 38 | Version: "v0.0.4", 39 | } 40 | 41 | // Execute adds all child commands to the root command and sets flags appropriately. 42 | // This is called by main.main(). It only needs to happen once to the rootCmd. 43 | func Execute() { 44 | if err := rootCmd.Execute(); err != nil { 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | func init() { 50 | cobra.OnInitialize( 51 | ensureElevenCLIRequirements, 52 | initializeElevenCLIConfig, 53 | ensureGitHubAccessTokenValidity, 54 | ) 55 | } 56 | 57 | func ensureElevenCLIRequirements() { 58 | missingRequirements := []string{} 59 | 60 | sshCommand := "ssh" 61 | _, err := exec.LookPath(sshCommand) 62 | 63 | if err != nil { 64 | missingRequirements = append( 65 | missingRequirements, 66 | fmt.Sprintf( 67 | "OpenSSH client (looked for an \"%s\" command)", 68 | sshCommand, 69 | ), 70 | ) 71 | } 72 | 73 | if len(missingRequirements) > 0 { 74 | elevenViewableErrorBuilder := dependencies.ProvideElevenViewableErrorBuilder() 75 | baseView := dependencies.ProvideBaseView() 76 | 77 | missingRequirementsErr := exceptions.ErrMissingRequirements{ 78 | MissingRequirements: missingRequirements, 79 | } 80 | 81 | baseView.ShowErrorViewWithStartingNewLine( 82 | elevenViewableErrorBuilder.Build( 83 | missingRequirementsErr, 84 | ), 85 | ) 86 | 87 | os.Exit(1) 88 | } 89 | } 90 | 91 | func initializeElevenCLIConfig() { 92 | configDir := system.UserConfigDir() 93 | configDirPerms := fs.FileMode(0700) 94 | 95 | // Ensure configuration dir exists 96 | err := os.MkdirAll( 97 | configDir, 98 | configDirPerms, 99 | ) 100 | cobra.CheckErr(err) 101 | 102 | configFilePath := system.UserConfigFilePath() 103 | configFilePerms := fs.FileMode(0600) 104 | 105 | // Ensure configuration file exists 106 | f, err := os.OpenFile( 107 | configFilePath, 108 | os.O_CREATE, 109 | configFilePerms, 110 | ) 111 | cobra.CheckErr(err) 112 | defer f.Close() 113 | 114 | viper.SetConfigFile(configFilePath) 115 | cobra.CheckErr(viper.ReadInConfig()) 116 | } 117 | 118 | // ensureGitHubAccessTokenValidity ensures that 119 | // the github access token has not been 120 | // revoked by user 121 | func ensureGitHubAccessTokenValidity() { 122 | userConfig := config.NewUserConfig() 123 | userIsLoggedIn := userConfig.GetBool(config.UserConfigKeyUserIsLoggedIn) 124 | 125 | if !userIsLoggedIn { 126 | return 127 | } 128 | 129 | gitHubService := github.NewService() 130 | 131 | githubUser, err := gitHubService.GetAuthenticatedUser( 132 | userConfig.GetString( 133 | config.UserConfigKeyGitHubAccessToken, 134 | ), 135 | ) 136 | 137 | if err != nil && 138 | gitHubService.IsInvalidAccessTokenError(err) { // User has revoked access token 139 | 140 | userIsLoggedIn = false 141 | 142 | userConfig.Set( 143 | config.UserConfigKeyUserIsLoggedIn, 144 | userIsLoggedIn, 145 | ) 146 | 147 | // Error is swallowed here to 148 | // not confuse user with unexpected error 149 | _ = userConfig.WriteConfig() 150 | } 151 | 152 | if err == nil { 153 | // Update config with updated values from GitHub 154 | userConfig.PopulateFromGitHubUser(githubUser) 155 | 156 | // Error is swallowed here to 157 | // not confuse user with unexpected error 158 | _ = userConfig.WriteConfig() 159 | } 160 | } 161 | 162 | func ensureUserIsLoggedIn() { 163 | userConfig := config.NewUserConfig() 164 | userIsLoggedIn := userConfig.GetBool(config.UserConfigKeyUserIsLoggedIn) 165 | 166 | if !userIsLoggedIn { 167 | elevenViewableErrorBuilder := dependencies.ProvideElevenViewableErrorBuilder() 168 | baseView := dependencies.ProvideBaseView() 169 | 170 | baseView.ShowErrorViewWithStartingNewLine( 171 | elevenViewableErrorBuilder.Build( 172 | exceptions.ErrUserNotLoggedIn, 173 | ), 174 | ) 175 | 176 | os.Exit(1) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /internal/cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/agent/config" 7 | "github.com/eleven-sh/cli/internal/dependencies" 8 | "github.com/eleven-sh/eleven/features" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func loadServeCmd( 13 | providerCmd *cobra.Command, 14 | provider *cloudProvider, 15 | ) { 16 | 17 | var binding string 18 | 19 | var serveCmd = &cobra.Command{ 20 | Use: "serve ", 21 | 22 | Short: "Allow TCP traffic on a port", 23 | 24 | Long: `Allow TCP traffic on a port. 25 | 26 | This command allows TCP traffic on a port in a sandbox. 27 | 28 | Once TCP traffic is allowed, the port becomes reachable from outside. 29 | 30 | To reach the port through a domain name (via HTTP(S)), use the "--as" flag.`, 31 | 32 | Example: ` eleven ` + provider.ShortName + ` serve eleven-api 8080 33 | eleven ` + provider.ShortName + ` serve eleven-api 8080 --as api.eleven.sh`, 34 | 35 | Args: cobra.ExactArgs(2), 36 | 37 | Run: func(cmd *cobra.Command, args []string) { 38 | envName := args[0] 39 | port := args[1] 40 | 41 | serveInput := features.ServeInput{ 42 | EnvName: envName, 43 | ReservedPorts: config.EnvReservedPorts, 44 | Port: port, 45 | PortBinding: binding, 46 | DomainReachabilityChecker: dependencies.ProvideDomainReachabilityChecker(), 47 | } 48 | 49 | serve := provider.ProvideServeFeature() 50 | 51 | if err := serve.Execute(serveInput); err != nil { 52 | os.Exit(1) 53 | } 54 | }, 55 | } 56 | 57 | serveCmd.Flags().StringVar( 58 | &binding, 59 | "as", 60 | "", 61 | "the domain name to use to reach the port", 62 | ) 63 | 64 | providerCmd.AddCommand(serveCmd) 65 | } 66 | -------------------------------------------------------------------------------- /internal/cmd/uninstall.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/eleven/features" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func loadUninstallCmd( 11 | providerCmd *cobra.Command, 12 | provider *cloudProvider, 13 | ) { 14 | 15 | var uninstallCmd = &cobra.Command{ 16 | Use: "uninstall", 17 | 18 | Short: "Uninstall Eleven", 19 | 20 | Long: `Uninstall Eleven. 21 | 22 | This command uninstall Eleven from your ` + provider.LongName + ` account. 23 | 24 | All your sandboxes must be removed before running this command.`, 25 | 26 | Example: " eleven " + provider.ShortName + " uninstall", 27 | 28 | Run: func(cmd *cobra.Command, args []string) { 29 | 30 | uninstallInput := features.UninstallInput{ 31 | SuccessMessage: provider.UninstallSuccessMessage, 32 | AlreadyUninstalledMessage: provider.UninstallAlreadyUninstalledMessage, 33 | } 34 | 35 | uninstall := provider.ProvideUninstallFeature() 36 | 37 | if err := uninstall.Execute(uninstallInput); err != nil { 38 | os.Exit(1) 39 | } 40 | }, 41 | } 42 | 43 | providerCmd.AddCommand(uninstallCmd) 44 | } 45 | -------------------------------------------------------------------------------- /internal/cmd/unserve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/agent/config" 7 | "github.com/eleven-sh/eleven/features" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func loadUnserveCmd( 12 | providerCmd *cobra.Command, 13 | provider *cloudProvider, 14 | ) { 15 | 16 | var unserveCmd = &cobra.Command{ 17 | Use: "unserve ", 18 | 19 | Short: "Disallow TCP traffic on a port", 20 | 21 | Long: `Disallow TCP traffic on a port. 22 | 23 | This command disallows TCP traffic on a port in a sandbox. 24 | 25 | Once TCP traffic is disallowed, the port becomes unreachable from outside.`, 26 | 27 | Example: " eleven " + provider.ShortName + " unserve eleven-api 8080", 28 | 29 | Args: cobra.ExactArgs(2), 30 | 31 | Run: func(cmd *cobra.Command, args []string) { 32 | envName := args[0] 33 | port := args[1] 34 | 35 | unserveInput := features.UnserveInput{ 36 | EnvName: envName, 37 | ReservedPorts: config.EnvReservedPorts, 38 | Port: port, 39 | } 40 | 41 | unserve := provider.ProvideUnserveFeature() 42 | 43 | if err := unserve.Execute(unserveInput); err != nil { 44 | os.Exit(1) 45 | } 46 | }, 47 | } 48 | 49 | providerCmd.AddCommand(unserveCmd) 50 | } 51 | -------------------------------------------------------------------------------- /internal/config/colors.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/jwalton/gchalk" 4 | 5 | var ( 6 | ColorsUnderline = gchalk.Underline 7 | ColorsBold = gchalk.Bold 8 | ColorsGreen = gchalk.Green 9 | ColorsBlue = gchalk.Cyan 10 | ColorsCyan = gchalk.Cyan 11 | ColorsRed = gchalk.Red 12 | ColorsYellow = gchalk.Yellow 13 | ColorsWhite = gchalk.BrightWhite 14 | ) 15 | 16 | var ( 17 | ColorsBGBlue = gchalk.BgCyan 18 | ) 19 | -------------------------------------------------------------------------------- /internal/config/github.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | GitHubOAuthClientID = "523ada8718f092d34c40" 5 | 6 | GitHubOAuthCLIToAPIURLPath = "/github/oauth/callback" 7 | GitHubOAuthCLIToAPIURL = "http://127.0.0.1:8080" + GitHubOAuthCLIToAPIURLPath 8 | 9 | GitHubOAuthAPIToCLIURLPath = "/github/oauth/callback" 10 | 11 | GitHubOAuthScopes = []string{ 12 | "read:user", 13 | "user:email", 14 | "repo", 15 | "admin:public_key", 16 | } 17 | ) 18 | -------------------------------------------------------------------------------- /internal/config/github_prod.go: -------------------------------------------------------------------------------- 1 | //go:build prod 2 | 3 | package config 4 | 5 | func init() { 6 | GitHubOAuthClientID = "eb67d11a0e1cf1824f09" 7 | 8 | GitHubOAuthCLIToAPIURL = "https://api.eleven.sh" + GitHubOAuthCLIToAPIURLPath 9 | } 10 | -------------------------------------------------------------------------------- /internal/config/user_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/eleven-sh/eleven/github" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | type UserConfigKey string 9 | 10 | const ( 11 | UserConfigKeyUserIsLoggedIn UserConfigKey = "user_is_logged_in" 12 | UserConfigKeyGitHubAccessToken UserConfigKey = "github_access_token" 13 | UserConfigKeyGitHubUsername UserConfigKey = "github_username" 14 | UserConfigKeyGitHubEmail UserConfigKey = "github_email" 15 | UserConfigKeyGitHubFullName UserConfigKey = "github_full_name" 16 | ) 17 | 18 | type UserConfig struct{} 19 | 20 | func NewUserConfig() UserConfig { 21 | return UserConfig{} 22 | } 23 | 24 | func (UserConfig) GetString(key UserConfigKey) string { 25 | return viper.GetString(string(key)) 26 | } 27 | 28 | func (UserConfig) GetBool(key UserConfigKey) bool { 29 | return viper.GetBool(string(key)) 30 | } 31 | 32 | func (UserConfig) Set(key UserConfigKey, value interface{}) { 33 | viper.Set(string(key), value) 34 | } 35 | 36 | func (u UserConfig) PopulateFromGitHubUser(githubUser *github.AuthenticatedUser) { 37 | u.Set( 38 | UserConfigKeyGitHubEmail, 39 | githubUser.PrimaryEmail, 40 | ) 41 | 42 | u.Set( 43 | UserConfigKeyGitHubFullName, 44 | githubUser.FullName, 45 | ) 46 | 47 | u.Set( 48 | UserConfigKeyGitHubUsername, 49 | githubUser.Username, 50 | ) 51 | } 52 | 53 | func (UserConfig) WriteConfig() error { 54 | return viper.WriteConfig() 55 | } 56 | -------------------------------------------------------------------------------- /internal/dependencies/aws_edit.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 9 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 10 | featuresCLI "github.com/eleven-sh/cli/internal/features" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/views" 13 | "github.com/eleven-sh/eleven/features" 14 | "github.com/google/wire" 15 | ) 16 | 17 | func ProvideAWSEditFeature(region, profile, credentialsFilePath, configFilePath string) features.EditFeature { 18 | return provideAWSEditFeature( 19 | awsProviderUserConfig.EnvVarsResolverOpts{ 20 | Region: region, 21 | }, 22 | 23 | awsProviderUserConfig.FilesResolverOpts{ 24 | Region: region, 25 | Profile: profile, 26 | CredentialsFilePath: credentialsFilePath, 27 | ConfigFilePath: configFilePath, 28 | }, 29 | 30 | awsCLI.UserConfigLocalResolverOpts{ 31 | Profile: profile, 32 | }, 33 | ) 34 | } 35 | 36 | func provideAWSEditFeature( 37 | userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, 38 | userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, 39 | userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, 40 | ) features.EditFeature { 41 | panic( 42 | wire.Build( 43 | viewSet, 44 | awsServiceBuilderSet, 45 | awsViewableErrorBuilder, 46 | 47 | loggerSet, 48 | 49 | stepperSet, 50 | 51 | vscodeProcessManagerSet, 52 | 53 | vscodeExtensionsManagerSet, 54 | 55 | wire.Bind(new(features.EditOutputHandler), new(featuresCLI.EditOutputHandler)), 56 | featuresCLI.NewEditOutputHandler, 57 | 58 | wire.Bind(new(featuresCLI.EditPresenter), new(presenters.EditPresenter)), 59 | presenters.NewEditPresenter, 60 | 61 | wire.Bind(new(presenters.EditViewer), new(views.EditView)), 62 | views.NewEditView, 63 | 64 | features.NewEditFeature, 65 | ), 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /internal/dependencies/aws_init.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 9 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 10 | featuresCLI "github.com/eleven-sh/cli/internal/features" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/views" 13 | "github.com/eleven-sh/eleven/features" 14 | "github.com/google/wire" 15 | ) 16 | 17 | func ProvideAWSInitFeature(region, profile, credentialsFilePath, configFilePath string) features.InitFeature { 18 | return provideAWSInitFeature( 19 | awsProviderUserConfig.EnvVarsResolverOpts{ 20 | Region: region, 21 | }, 22 | 23 | awsProviderUserConfig.FilesResolverOpts{ 24 | Region: region, 25 | Profile: profile, 26 | CredentialsFilePath: credentialsFilePath, 27 | ConfigFilePath: configFilePath, 28 | }, 29 | 30 | awsCLI.UserConfigLocalResolverOpts{ 31 | Profile: profile, 32 | }, 33 | ) 34 | } 35 | 36 | func provideAWSInitFeature( 37 | userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, 38 | userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, 39 | userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, 40 | ) features.InitFeature { 41 | panic( 42 | wire.Build( 43 | viewSet, 44 | awsServiceBuilderSet, 45 | awsViewableErrorBuilder, 46 | 47 | userConfigManagerSet, 48 | 49 | agentSet, 50 | 51 | githubManagerSet, 52 | 53 | loggerSet, 54 | 55 | sshConfigManagerSet, 56 | 57 | sshKnownHostsManagerSet, 58 | 59 | sshKeysManagerSet, 60 | 61 | stepperSet, 62 | 63 | wire.Bind(new(features.InitOutputHandler), new(featuresCLI.InitOutputHandler)), 64 | featuresCLI.NewInitOutputHandler, 65 | 66 | wire.Bind(new(featuresCLI.InitPresenter), new(presenters.InitPresenter)), 67 | presenters.NewInitPresenter, 68 | 69 | wire.Bind(new(presenters.InitViewer), new(views.InitView)), 70 | views.NewInitView, 71 | 72 | features.NewInitFeature, 73 | ), 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /internal/dependencies/aws_remove.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 9 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 10 | featuresCLI "github.com/eleven-sh/cli/internal/features" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/views" 13 | "github.com/eleven-sh/eleven/features" 14 | "github.com/google/wire" 15 | ) 16 | 17 | func ProvideAWSRemoveFeature(region, profile, credentialsFilePath, configFilePath string) features.RemoveFeature { 18 | return provideAWSRemoveFeature( 19 | awsProviderUserConfig.EnvVarsResolverOpts{ 20 | Region: region, 21 | }, 22 | 23 | awsProviderUserConfig.FilesResolverOpts{ 24 | Region: region, 25 | Profile: profile, 26 | CredentialsFilePath: credentialsFilePath, 27 | ConfigFilePath: configFilePath, 28 | }, 29 | 30 | awsCLI.UserConfigLocalResolverOpts{ 31 | Profile: profile, 32 | }, 33 | ) 34 | } 35 | 36 | func provideAWSRemoveFeature( 37 | userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, 38 | userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, 39 | userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, 40 | ) features.RemoveFeature { 41 | panic( 42 | wire.Build( 43 | viewSet, 44 | awsServiceBuilderSet, 45 | awsViewableErrorBuilder, 46 | 47 | loggerSet, 48 | 49 | stepperSet, 50 | 51 | wire.Bind(new(features.RemoveOutputHandler), new(featuresCLI.RemoveOutputHandler)), 52 | featuresCLI.NewRemoveOutputHandler, 53 | 54 | wire.Bind(new(featuresCLI.RemovePresenter), new(presenters.RemovePresenter)), 55 | presenters.NewRemovePresenter, 56 | 57 | wire.Bind(new(presenters.RemoveViewer), new(views.RemoveView)), 58 | views.NewRemoveView, 59 | 60 | features.NewRemoveFeature, 61 | ), 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /internal/dependencies/aws_serve.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 9 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 10 | featuresCLI "github.com/eleven-sh/cli/internal/features" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/views" 13 | "github.com/eleven-sh/eleven/features" 14 | "github.com/google/wire" 15 | ) 16 | 17 | func ProvideAWSServeFeature(region, profile, credentialsFilePath, configFilePath string) features.ServeFeature { 18 | return provideAWSServeFeature( 19 | awsProviderUserConfig.EnvVarsResolverOpts{ 20 | Region: region, 21 | }, 22 | 23 | awsProviderUserConfig.FilesResolverOpts{ 24 | Region: region, 25 | Profile: profile, 26 | CredentialsFilePath: credentialsFilePath, 27 | ConfigFilePath: configFilePath, 28 | }, 29 | 30 | awsCLI.UserConfigLocalResolverOpts{ 31 | Profile: profile, 32 | }, 33 | ) 34 | } 35 | 36 | func provideAWSServeFeature( 37 | userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, 38 | userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, 39 | userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, 40 | ) features.ServeFeature { 41 | panic( 42 | wire.Build( 43 | viewSet, 44 | awsServiceBuilderSet, 45 | awsViewableErrorBuilder, 46 | 47 | loggerSet, 48 | 49 | stepperSet, 50 | 51 | agentSet, 52 | 53 | wire.Bind(new(features.ServeOutputHandler), new(featuresCLI.ServeOutputHandler)), 54 | featuresCLI.NewServeOutputHandler, 55 | 56 | wire.Bind(new(featuresCLI.ServePresenter), new(presenters.ServePresenter)), 57 | presenters.NewServePresenter, 58 | 59 | wire.Bind(new(presenters.ServeViewer), new(views.ServeView)), 60 | views.NewServeView, 61 | 62 | features.NewServeFeature, 63 | ), 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /internal/dependencies/aws_shared.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderConfig "github.com/eleven-sh/aws-cloud-provider/config" 9 | awsProviderService "github.com/eleven-sh/aws-cloud-provider/service" 10 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 11 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 12 | "github.com/eleven-sh/cli/internal/presenters" 13 | "github.com/eleven-sh/cli/internal/system" 14 | "github.com/eleven-sh/eleven/entities" 15 | "github.com/google/wire" 16 | ) 17 | 18 | var awsViewableErrorBuilder = wire.NewSet( 19 | wire.Bind(new(presenters.ViewableErrorBuilder), new(awsCLI.AWSViewableErrorBuilder)), 20 | awsCLI.NewAWSViewableErrorBuilder, 21 | ) 22 | 23 | var awsServiceBuilderSet = wire.NewSet( 24 | wire.Bind(new(awsProviderUserConfig.ProfileLoader), new(awsProviderConfig.ProfileLoader)), 25 | awsProviderConfig.NewProfileLoader, 26 | 27 | wire.Bind(new(awsCLI.UserConfigFilesResolver), new(awsProviderUserConfig.FilesResolver)), 28 | awsProviderUserConfig.NewFilesResolver, 29 | 30 | wire.Bind(new(awsProviderUserConfig.EnvVarsGetter), new(system.EnvVars)), 31 | system.NewEnvVars, 32 | 33 | wire.Bind(new(awsCLI.UserConfigEnvVarsResolver), new(awsProviderUserConfig.EnvVarsResolver)), 34 | awsProviderUserConfig.NewEnvVarsResolver, 35 | 36 | wire.Bind(new(awsProviderService.UserConfigResolver), new(awsCLI.UserConfigLocalResolver)), 37 | awsCLI.NewUserConfigLocalResolver, 38 | 39 | wire.Bind(new(awsProviderService.UserConfigValidator), new(awsProviderConfig.UserConfigValidator)), 40 | awsProviderConfig.NewUserConfigValidator, 41 | 42 | wire.Bind(new(awsProviderService.UserConfigLoader), new(awsProviderConfig.UserConfigLoader)), 43 | awsProviderConfig.NewUserConfigLoader, 44 | 45 | wire.Bind(new(entities.CloudServiceBuilder), new(awsProviderService.Builder)), 46 | awsProviderService.NewBuilder, 47 | ) 48 | -------------------------------------------------------------------------------- /internal/dependencies/aws_uninstall.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 9 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 10 | featuresCLI "github.com/eleven-sh/cli/internal/features" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/views" 13 | "github.com/eleven-sh/eleven/features" 14 | "github.com/google/wire" 15 | ) 16 | 17 | func ProvideAWSUninstallFeature(region, profile, credentialsFilePath, configFilePath string) features.UninstallFeature { 18 | return provideAWSUninstallFeature( 19 | awsProviderUserConfig.EnvVarsResolverOpts{ 20 | Region: region, 21 | }, 22 | 23 | awsProviderUserConfig.FilesResolverOpts{ 24 | Region: region, 25 | Profile: profile, 26 | CredentialsFilePath: credentialsFilePath, 27 | ConfigFilePath: configFilePath, 28 | }, 29 | 30 | awsCLI.UserConfigLocalResolverOpts{ 31 | Profile: profile, 32 | }, 33 | ) 34 | } 35 | 36 | func provideAWSUninstallFeature( 37 | userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, 38 | userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, 39 | userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, 40 | ) features.UninstallFeature { 41 | panic( 42 | wire.Build( 43 | viewSet, 44 | awsServiceBuilderSet, 45 | awsViewableErrorBuilder, 46 | 47 | loggerSet, 48 | 49 | stepperSet, 50 | 51 | wire.Bind(new(features.UninstallOutputHandler), new(featuresCLI.UninstallOutputHandler)), 52 | featuresCLI.NewUninstallOutputHandler, 53 | 54 | wire.Bind(new(featuresCLI.UninstallPresenter), new(presenters.UninstallPresenter)), 55 | presenters.NewUninstallPresenter, 56 | 57 | wire.Bind(new(presenters.UninstallViewer), new(views.UninstallView)), 58 | views.NewUninstallView, 59 | 60 | features.NewUninstallFeature, 61 | ), 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /internal/dependencies/aws_unserve.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | awsProviderUserConfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 9 | awsCLI "github.com/eleven-sh/cli/internal/cloudproviders/aws" 10 | featuresCLI "github.com/eleven-sh/cli/internal/features" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/views" 13 | "github.com/eleven-sh/eleven/features" 14 | "github.com/google/wire" 15 | ) 16 | 17 | func ProvideAWSUnserveFeature(region, profile, credentialsFilePath, configFilePath string) features.UnserveFeature { 18 | return provideAWSUnserveFeature( 19 | awsProviderUserConfig.EnvVarsResolverOpts{ 20 | Region: region, 21 | }, 22 | 23 | awsProviderUserConfig.FilesResolverOpts{ 24 | Region: region, 25 | Profile: profile, 26 | CredentialsFilePath: credentialsFilePath, 27 | ConfigFilePath: configFilePath, 28 | }, 29 | 30 | awsCLI.UserConfigLocalResolverOpts{ 31 | Profile: profile, 32 | }, 33 | ) 34 | } 35 | 36 | func provideAWSUnserveFeature( 37 | userConfigEnvVarsResolverOpts awsProviderUserConfig.EnvVarsResolverOpts, 38 | userConfigFilesResolverOpts awsProviderUserConfig.FilesResolverOpts, 39 | userConfigLocalResolverOpts awsCLI.UserConfigLocalResolverOpts, 40 | ) features.UnserveFeature { 41 | panic( 42 | wire.Build( 43 | viewSet, 44 | awsServiceBuilderSet, 45 | awsViewableErrorBuilder, 46 | 47 | loggerSet, 48 | 49 | stepperSet, 50 | 51 | agentSet, 52 | 53 | wire.Bind(new(features.UnserveOutputHandler), new(featuresCLI.UnserveOutputHandler)), 54 | featuresCLI.NewUnserveOutputHandler, 55 | 56 | wire.Bind(new(featuresCLI.UnservePresenter), new(presenters.UnservePresenter)), 57 | presenters.NewUnservePresenter, 58 | 59 | wire.Bind(new(presenters.UnserveViewer), new(views.UnserveView)), 60 | views.NewUnserveView, 61 | 62 | features.NewUnserveFeature, 63 | ), 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /internal/dependencies/entities.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | "github.com/eleven-sh/cli/internal/entities" 9 | "github.com/google/wire" 10 | ) 11 | 12 | func ProvideEnvRepositoriesResolver() entities.EnvRepositoriesResolver { 13 | panic( 14 | wire.Build( 15 | loggerSet, 16 | 17 | userConfigManagerSet, 18 | 19 | githubManagerSet, 20 | 21 | entities.NewEnvRepositoriesResolver, 22 | ), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_edit.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | featuresCLI "github.com/eleven-sh/cli/internal/features" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/features" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | func ProvideHetznerEditFeature(elevenConfigDir, region, context string) features.EditFeature { 19 | return provideHetznerEditFeature( 20 | hetznerProviderUserConfig.EnvVarsResolverOpts{ 21 | Region: region, 22 | }, 23 | 24 | hetznerProviderUserConfig.FilesResolverOpts{ 25 | Region: region, 26 | Context: context, 27 | }, 28 | 29 | hetznerCLI.UserConfigLocalResolverOpts{ 30 | Context: context, 31 | }, 32 | 33 | hetznerProviderService.BuilderOpts{ 34 | ElevenConfigDir: elevenConfigDir, 35 | }, 36 | ) 37 | } 38 | 39 | func provideHetznerEditFeature( 40 | userConfigEnvVarsResolverOpts hetznerProviderUserConfig.EnvVarsResolverOpts, 41 | userConfigFilesResolverOpts hetznerProviderUserConfig.FilesResolverOpts, 42 | userConfigLocalResolverOpts hetznerCLI.UserConfigLocalResolverOpts, 43 | serviceBuilderOpts hetznerProviderService.BuilderOpts, 44 | ) features.EditFeature { 45 | panic( 46 | wire.Build( 47 | viewSet, 48 | hetznerServiceBuilderSet, 49 | hetznerViewableErrorBuilder, 50 | 51 | loggerSet, 52 | 53 | stepperSet, 54 | 55 | vscodeProcessManagerSet, 56 | 57 | vscodeExtensionsManagerSet, 58 | 59 | wire.Bind(new(features.EditOutputHandler), new(featuresCLI.EditOutputHandler)), 60 | featuresCLI.NewEditOutputHandler, 61 | 62 | wire.Bind(new(featuresCLI.EditPresenter), new(presenters.EditPresenter)), 63 | presenters.NewEditPresenter, 64 | 65 | wire.Bind(new(presenters.EditViewer), new(views.EditView)), 66 | views.NewEditView, 67 | 68 | features.NewEditFeature, 69 | ), 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_init.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | featuresCLI "github.com/eleven-sh/cli/internal/features" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/features" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | func ProvideHetznerInitFeature(elevenConfigDir, region, context string) features.InitFeature { 19 | return provideHetznerInitFeature( 20 | hetznerProviderUserConfig.EnvVarsResolverOpts{ 21 | Region: region, 22 | }, 23 | 24 | hetznerProviderUserConfig.FilesResolverOpts{ 25 | Region: region, 26 | Context: context, 27 | }, 28 | 29 | hetznerCLI.UserConfigLocalResolverOpts{ 30 | Context: context, 31 | }, 32 | 33 | hetznerProviderService.BuilderOpts{ 34 | ElevenConfigDir: elevenConfigDir, 35 | }, 36 | ) 37 | } 38 | 39 | func provideHetznerInitFeature( 40 | userConfigEnvVarsResolverOpts hetznerProviderUserConfig.EnvVarsResolverOpts, 41 | userConfigFilesResolverOpts hetznerProviderUserConfig.FilesResolverOpts, 42 | userConfigLocalResolverOpts hetznerCLI.UserConfigLocalResolverOpts, 43 | serviceBuilderOpts hetznerProviderService.BuilderOpts, 44 | ) features.InitFeature { 45 | panic( 46 | wire.Build( 47 | viewSet, 48 | hetznerServiceBuilderSet, 49 | hetznerViewableErrorBuilder, 50 | 51 | userConfigManagerSet, 52 | 53 | agentSet, 54 | 55 | githubManagerSet, 56 | 57 | loggerSet, 58 | 59 | sshConfigManagerSet, 60 | 61 | sshKnownHostsManagerSet, 62 | 63 | sshKeysManagerSet, 64 | 65 | stepperSet, 66 | 67 | wire.Bind(new(features.InitOutputHandler), new(featuresCLI.InitOutputHandler)), 68 | featuresCLI.NewInitOutputHandler, 69 | 70 | wire.Bind(new(featuresCLI.InitPresenter), new(presenters.InitPresenter)), 71 | presenters.NewInitPresenter, 72 | 73 | wire.Bind(new(presenters.InitViewer), new(views.InitView)), 74 | views.NewInitView, 75 | 76 | features.NewInitFeature, 77 | ), 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_remove.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | featuresCLI "github.com/eleven-sh/cli/internal/features" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/features" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | func ProvideHetznerRemoveFeature(elevenConfigDir, region, context string) features.RemoveFeature { 19 | return provideHetznerRemoveFeature( 20 | hetznerProviderUserConfig.EnvVarsResolverOpts{ 21 | Region: region, 22 | }, 23 | 24 | hetznerProviderUserConfig.FilesResolverOpts{ 25 | Region: region, 26 | Context: context, 27 | }, 28 | 29 | hetznerCLI.UserConfigLocalResolverOpts{ 30 | Context: context, 31 | }, 32 | 33 | hetznerProviderService.BuilderOpts{ 34 | ElevenConfigDir: elevenConfigDir, 35 | }, 36 | ) 37 | } 38 | 39 | func provideHetznerRemoveFeature( 40 | userConfigEnvVarsResolverOpts hetznerProviderUserConfig.EnvVarsResolverOpts, 41 | userConfigFilesResolverOpts hetznerProviderUserConfig.FilesResolverOpts, 42 | userConfigLocalResolverOpts hetznerCLI.UserConfigLocalResolverOpts, 43 | serviceBuilderOpts hetznerProviderService.BuilderOpts, 44 | ) features.RemoveFeature { 45 | panic( 46 | wire.Build( 47 | viewSet, 48 | hetznerServiceBuilderSet, 49 | hetznerViewableErrorBuilder, 50 | 51 | loggerSet, 52 | 53 | stepperSet, 54 | 55 | wire.Bind(new(features.RemoveOutputHandler), new(featuresCLI.RemoveOutputHandler)), 56 | featuresCLI.NewRemoveOutputHandler, 57 | 58 | wire.Bind(new(featuresCLI.RemovePresenter), new(presenters.RemovePresenter)), 59 | presenters.NewRemovePresenter, 60 | 61 | wire.Bind(new(presenters.RemoveViewer), new(views.RemoveView)), 62 | views.NewRemoveView, 63 | 64 | features.NewRemoveFeature, 65 | ), 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_serve.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | featuresCLI "github.com/eleven-sh/cli/internal/features" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/features" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | func ProvideHetznerServeFeature(elevenConfigDir, region, context string) features.ServeFeature { 19 | return provideHetznerServeFeature( 20 | hetznerProviderUserConfig.EnvVarsResolverOpts{ 21 | Region: region, 22 | }, 23 | 24 | hetznerProviderUserConfig.FilesResolverOpts{ 25 | Region: region, 26 | Context: context, 27 | }, 28 | 29 | hetznerCLI.UserConfigLocalResolverOpts{ 30 | Context: context, 31 | }, 32 | 33 | hetznerProviderService.BuilderOpts{ 34 | ElevenConfigDir: elevenConfigDir, 35 | }, 36 | ) 37 | } 38 | 39 | func provideHetznerServeFeature( 40 | userConfigEnvVarsResolverOpts hetznerProviderUserConfig.EnvVarsResolverOpts, 41 | userConfigFilesResolverOpts hetznerProviderUserConfig.FilesResolverOpts, 42 | userConfigLocalResolverOpts hetznerCLI.UserConfigLocalResolverOpts, 43 | serviceBuilderOpts hetznerProviderService.BuilderOpts, 44 | ) features.ServeFeature { 45 | panic( 46 | wire.Build( 47 | viewSet, 48 | hetznerServiceBuilderSet, 49 | hetznerViewableErrorBuilder, 50 | 51 | loggerSet, 52 | 53 | stepperSet, 54 | 55 | agentSet, 56 | 57 | wire.Bind(new(features.ServeOutputHandler), new(featuresCLI.ServeOutputHandler)), 58 | featuresCLI.NewServeOutputHandler, 59 | 60 | wire.Bind(new(featuresCLI.ServePresenter), new(presenters.ServePresenter)), 61 | presenters.NewServePresenter, 62 | 63 | wire.Bind(new(presenters.ServeViewer), new(views.ServeView)), 64 | views.NewServeView, 65 | 66 | features.NewServeFeature, 67 | ), 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_shared.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | "github.com/eleven-sh/cli/internal/presenters" 10 | "github.com/eleven-sh/cli/internal/system" 11 | "github.com/eleven-sh/eleven/entities" 12 | hetznerProviderConfig "github.com/eleven-sh/hetzner-cloud-provider/config" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | var hetznerViewableErrorBuilder = wire.NewSet( 19 | wire.Bind(new(presenters.ViewableErrorBuilder), new(hetznerCLI.HetznerViewableErrorBuilder)), 20 | hetznerCLI.NewHetznerViewableErrorBuilder, 21 | ) 22 | 23 | var hetznerServiceBuilderSet = wire.NewSet( 24 | wire.Bind(new(hetznerProviderUserConfig.ContextLoader), new(hetznerProviderConfig.ContextLoader)), 25 | hetznerProviderConfig.NewContextLoader, 26 | 27 | wire.Bind(new(hetznerCLI.UserConfigFilesResolver), new(hetznerProviderUserConfig.FilesResolver)), 28 | hetznerProviderUserConfig.NewFilesResolver, 29 | 30 | wire.Bind(new(hetznerProviderUserConfig.EnvVarsGetter), new(system.EnvVars)), 31 | system.NewEnvVars, 32 | 33 | wire.Bind(new(hetznerCLI.UserConfigEnvVarsResolver), new(hetznerProviderUserConfig.EnvVarsResolver)), 34 | hetznerProviderUserConfig.NewEnvVarsResolver, 35 | 36 | wire.Bind(new(hetznerProviderService.UserConfigResolver), new(hetznerCLI.UserConfigLocalResolver)), 37 | hetznerCLI.NewUserConfigLocalResolver, 38 | 39 | wire.Bind(new(hetznerProviderService.UserConfigValidator), new(hetznerProviderConfig.UserConfigValidator)), 40 | hetznerProviderConfig.NewUserConfigValidator, 41 | 42 | wire.Bind(new(entities.CloudServiceBuilder), new(hetznerProviderService.Builder)), 43 | hetznerProviderService.NewBuilder, 44 | ) 45 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_uninstall.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | featuresCLI "github.com/eleven-sh/cli/internal/features" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/features" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | func ProvideHetznerUninstallFeature(elevenConfigDir, region, context string) features.UninstallFeature { 19 | return provideHetznerUninstallFeature( 20 | hetznerProviderUserConfig.EnvVarsResolverOpts{ 21 | Region: region, 22 | }, 23 | 24 | hetznerProviderUserConfig.FilesResolverOpts{ 25 | Region: region, 26 | Context: context, 27 | }, 28 | 29 | hetznerCLI.UserConfigLocalResolverOpts{ 30 | Context: context, 31 | }, 32 | 33 | hetznerProviderService.BuilderOpts{ 34 | ElevenConfigDir: elevenConfigDir, 35 | }, 36 | ) 37 | } 38 | 39 | func provideHetznerUninstallFeature( 40 | userConfigEnvVarsResolverOpts hetznerProviderUserConfig.EnvVarsResolverOpts, 41 | userConfigFilesResolverOpts hetznerProviderUserConfig.FilesResolverOpts, 42 | userConfigLocalResolverOpts hetznerCLI.UserConfigLocalResolverOpts, 43 | serviceBuilderOpts hetznerProviderService.BuilderOpts, 44 | ) features.UninstallFeature { 45 | panic( 46 | wire.Build( 47 | viewSet, 48 | hetznerServiceBuilderSet, 49 | hetznerViewableErrorBuilder, 50 | 51 | loggerSet, 52 | 53 | stepperSet, 54 | 55 | wire.Bind(new(features.UninstallOutputHandler), new(featuresCLI.UninstallOutputHandler)), 56 | featuresCLI.NewUninstallOutputHandler, 57 | 58 | wire.Bind(new(featuresCLI.UninstallPresenter), new(presenters.UninstallPresenter)), 59 | presenters.NewUninstallPresenter, 60 | 61 | wire.Bind(new(presenters.UninstallViewer), new(views.UninstallView)), 62 | views.NewUninstallView, 63 | 64 | features.NewUninstallFeature, 65 | ), 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /internal/dependencies/hetzner_unserve.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | hetznerCLI "github.com/eleven-sh/cli/internal/cloudproviders/hetzner" 9 | featuresCLI "github.com/eleven-sh/cli/internal/features" 10 | "github.com/eleven-sh/cli/internal/presenters" 11 | "github.com/eleven-sh/cli/internal/views" 12 | "github.com/eleven-sh/eleven/features" 13 | hetznerProviderService "github.com/eleven-sh/hetzner-cloud-provider/service" 14 | hetznerProviderUserConfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 15 | "github.com/google/wire" 16 | ) 17 | 18 | func ProvideHetznerUnserveFeature(elevenConfigDir, region, context string) features.UnserveFeature { 19 | return provideHetznerUnserveFeature( 20 | hetznerProviderUserConfig.EnvVarsResolverOpts{ 21 | Region: region, 22 | }, 23 | 24 | hetznerProviderUserConfig.FilesResolverOpts{ 25 | Region: region, 26 | Context: context, 27 | }, 28 | 29 | hetznerCLI.UserConfigLocalResolverOpts{ 30 | Context: context, 31 | }, 32 | 33 | hetznerProviderService.BuilderOpts{ 34 | ElevenConfigDir: elevenConfigDir, 35 | }, 36 | ) 37 | } 38 | 39 | func provideHetznerUnserveFeature( 40 | userConfigEnvVarsResolverOpts hetznerProviderUserConfig.EnvVarsResolverOpts, 41 | userConfigFilesResolverOpts hetznerProviderUserConfig.FilesResolverOpts, 42 | userConfigLocalResolverOpts hetznerCLI.UserConfigLocalResolverOpts, 43 | serviceBuilderOpts hetznerProviderService.BuilderOpts, 44 | ) features.UnserveFeature { 45 | panic( 46 | wire.Build( 47 | viewSet, 48 | hetznerServiceBuilderSet, 49 | hetznerViewableErrorBuilder, 50 | 51 | loggerSet, 52 | 53 | stepperSet, 54 | 55 | agentSet, 56 | 57 | wire.Bind(new(features.UnserveOutputHandler), new(featuresCLI.UnserveOutputHandler)), 58 | featuresCLI.NewUnserveOutputHandler, 59 | 60 | wire.Bind(new(featuresCLI.UnservePresenter), new(presenters.UnservePresenter)), 61 | presenters.NewUnservePresenter, 62 | 63 | wire.Bind(new(presenters.UnserveViewer), new(views.UnserveView)), 64 | views.NewUnserveView, 65 | 66 | features.NewUnserveFeature, 67 | ), 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /internal/dependencies/hooks.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | "github.com/eleven-sh/cli/internal/hooks" 9 | "github.com/google/wire" 10 | ) 11 | 12 | func ProvidePreRemoveHook() hooks.PreRemove { 13 | panic( 14 | wire.Build( 15 | sshConfigManagerSet, 16 | 17 | sshKnownHostsManagerSet, 18 | 19 | sshKeysManagerSet, 20 | 21 | userConfigManagerSet, 22 | 23 | githubManagerSet, 24 | 25 | hooks.NewPreRemove, 26 | ), 27 | ) 28 | } 29 | 30 | func ProvideDomainReachabilityChecker() hooks.DomainReachabilityChecker { 31 | panic( 32 | wire.Build( 33 | agentSet, 34 | 35 | hooks.NewDomainReachabilityChecker, 36 | ), 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /internal/dependencies/login.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | "github.com/eleven-sh/cli/internal/features" 9 | "github.com/eleven-sh/cli/internal/presenters" 10 | "github.com/eleven-sh/cli/internal/views" 11 | "github.com/google/wire" 12 | ) 13 | 14 | func ProvideLoginFeature() features.LoginFeature { 15 | panic( 16 | wire.Build( 17 | viewSet, 18 | elevenViewableErrorBuilder, 19 | 20 | loggerSet, 21 | 22 | browserManagerSet, 23 | 24 | userConfigManagerSet, 25 | 26 | sleeperSet, 27 | 28 | githubManagerSet, 29 | 30 | wire.Bind(new(features.LoginPresenter), new(presenters.LoginPresenter)), 31 | presenters.NewLoginPresenter, 32 | 33 | wire.Bind(new(presenters.LoginViewer), new(views.LoginView)), 34 | views.NewLoginView, 35 | 36 | features.NewLoginFeature, 37 | ), 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /internal/dependencies/shared.go: -------------------------------------------------------------------------------- 1 | // go:build wireinject 2 | //go:build wireinject 3 | // +build wireinject 4 | 5 | package dependencies 6 | 7 | import ( 8 | "github.com/eleven-sh/cli/internal/agent" 9 | "github.com/eleven-sh/cli/internal/config" 10 | "github.com/eleven-sh/cli/internal/interfaces" 11 | "github.com/eleven-sh/cli/internal/presenters" 12 | "github.com/eleven-sh/cli/internal/ssh" 13 | stepperCLI "github.com/eleven-sh/cli/internal/stepper" 14 | "github.com/eleven-sh/cli/internal/system" 15 | "github.com/eleven-sh/cli/internal/views" 16 | "github.com/eleven-sh/cli/internal/vscode" 17 | "github.com/eleven-sh/eleven/github" 18 | "github.com/eleven-sh/eleven/stepper" 19 | "github.com/google/wire" 20 | ) 21 | 22 | var viewSet = wire.NewSet( 23 | wire.Bind(new(views.Displayer), new(system.Displayer)), 24 | system.NewDisplayer, 25 | views.NewBaseView, 26 | ) 27 | 28 | func ProvideBaseView() views.BaseView { 29 | panic( 30 | wire.Build( 31 | viewSet, 32 | ), 33 | ) 34 | } 35 | 36 | var elevenViewableErrorBuilder = wire.NewSet( 37 | wire.Bind(new(presenters.ViewableErrorBuilder), new(presenters.ElevenViewableErrorBuilder)), 38 | presenters.NewElevenViewableErrorBuilder, 39 | ) 40 | 41 | func ProvideElevenViewableErrorBuilder() presenters.ElevenViewableErrorBuilder { 42 | panic( 43 | wire.Build( 44 | elevenViewableErrorBuilder, 45 | ), 46 | ) 47 | } 48 | 49 | var githubManagerSet = wire.NewSet( 50 | wire.Bind(new(interfaces.GitHubManager), new(github.Service)), 51 | github.NewService, 52 | ) 53 | 54 | var userConfigManagerSet = wire.NewSet( 55 | wire.Bind(new(interfaces.UserConfigManager), new(config.UserConfig)), 56 | config.NewUserConfig, 57 | ) 58 | 59 | var loggerSet = wire.NewSet( 60 | wire.Bind(new(interfaces.Logger), new(system.Logger)), 61 | system.NewLogger, 62 | ) 63 | 64 | var sshConfigManagerSet = wire.NewSet( 65 | wire.Bind(new(interfaces.SSHConfigManager), new(ssh.Config)), 66 | ssh.NewConfigWithDefaultConfigFilePath, 67 | ) 68 | 69 | func ProvideSSHConfigManager() ssh.Config { 70 | panic( 71 | wire.Build( 72 | sshConfigManagerSet, 73 | ), 74 | ) 75 | } 76 | 77 | var sshKnownHostsManagerSet = wire.NewSet( 78 | wire.Bind(new(interfaces.SSHKnownHostsManager), new(ssh.KnownHosts)), 79 | ssh.NewKnownHostsWithDefaultKnownHostsFilePath, 80 | ) 81 | 82 | var sshKeysManagerSet = wire.NewSet( 83 | wire.Bind(new(interfaces.SSHKeysManager), new(ssh.Keys)), 84 | ssh.NewKeysWithDefaultDir, 85 | ) 86 | 87 | var vscodeProcessManagerSet = wire.NewSet( 88 | wire.Bind(new(interfaces.VSCodeProcessManager), new(vscode.Process)), 89 | vscode.NewProcess, 90 | ) 91 | 92 | var vscodeExtensionsManagerSet = wire.NewSet( 93 | wire.Bind(new(interfaces.VSCodeExtensionsManager), new(vscode.Extensions)), 94 | vscode.NewExtensions, 95 | ) 96 | 97 | var browserManagerSet = wire.NewSet( 98 | wire.Bind(new(interfaces.BrowserManager), new(system.Browser)), 99 | system.NewBrowser, 100 | ) 101 | 102 | var sleeperSet = wire.NewSet( 103 | wire.Bind(new(interfaces.Sleeper), new(system.Sleeper)), 104 | system.NewSleeper, 105 | ) 106 | 107 | var stepperSet = wire.NewSet( 108 | wire.Bind(new(stepper.Stepper), new(stepperCLI.Stepper)), 109 | stepperCLI.NewStepper, 110 | ) 111 | 112 | var agentSet = wire.NewSet( 113 | wire.Bind(new(agent.ClientBuilder), new(agent.DefaultClientBuilder)), 114 | agent.NewDefaultClientBuilder, 115 | ) 116 | -------------------------------------------------------------------------------- /internal/entities/env.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type EnvAdditionalProperties struct { 4 | GitHubCreatedSSHKeyId *int64 `json:"github_created_ssh_key_id"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/entities/env_repositories.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/eleven-sh/agent/proto" 5 | "github.com/eleven-sh/cli/internal/config" 6 | "github.com/eleven-sh/cli/internal/interfaces" 7 | "github.com/eleven-sh/eleven/entities" 8 | "github.com/eleven-sh/eleven/github" 9 | ) 10 | 11 | type EnvRepositoriesResolver struct { 12 | logger interfaces.Logger 13 | userConfig interfaces.UserConfigManager 14 | github interfaces.GitHubManager 15 | } 16 | 17 | func NewEnvRepositoriesResolver( 18 | logger interfaces.Logger, 19 | userConfig interfaces.UserConfigManager, 20 | github interfaces.GitHubManager, 21 | ) EnvRepositoriesResolver { 22 | 23 | return EnvRepositoriesResolver{ 24 | logger: logger, 25 | userConfig: userConfig, 26 | github: github, 27 | } 28 | } 29 | 30 | func (e EnvRepositoriesResolver) Resolve( 31 | repositories []string, 32 | checkForRepositoryExistence bool, 33 | ) ([]entities.EnvRepository, error) { 34 | 35 | githubUsername := e.userConfig.GetString( 36 | config.UserConfigKeyGitHubUsername, 37 | ) 38 | 39 | resolvedRepos, err := resolveRepositories( 40 | repositories, 41 | githubUsername, 42 | ) 43 | 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | if checkForRepositoryExistence { 49 | 50 | githubAccessToken := e.userConfig.GetString( 51 | config.UserConfigKeyGitHubAccessToken, 52 | ) 53 | 54 | for _, repository := range resolvedRepos { 55 | repoExists, err := e.github.DoesRepositoryExist( 56 | githubAccessToken, 57 | repository.Owner, 58 | repository.Name, 59 | ) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if !repoExists { 66 | return nil, entities.ErrEnvRepositoryNotFound{ 67 | RepoOwner: repository.Owner, 68 | RepoName: repository.Name, 69 | } 70 | } 71 | } 72 | } 73 | 74 | return resolvedRepos, nil 75 | } 76 | 77 | func resolveRepositories( 78 | repositories []string, 79 | githubUsername string, 80 | ) ([]entities.EnvRepository, error) { 81 | 82 | resolvedRepos := []entities.EnvRepository{} 83 | alreadyResolvedRepos := map[string]bool{} 84 | 85 | for _, repositoryName := range repositories { 86 | parsedRepoName, err := github.ParseRepositoryName( 87 | repositoryName, 88 | githubUsername, 89 | ) 90 | 91 | if err != nil { 92 | // If the repository name is invalid, we are sure 93 | // that the repository doesn't exist 94 | return nil, entities.ErrEnvRepositoryNotFound{ 95 | RepoOwner: githubUsername, 96 | RepoName: repositoryName, 97 | } 98 | } 99 | 100 | repoUniqueKey := parsedRepoName.Owner + parsedRepoName.Name 101 | 102 | if _, alreadyResolved := alreadyResolvedRepos[repoUniqueKey]; alreadyResolved { 103 | return nil, entities.ErrEnvDuplicatedRepositories{ 104 | RepoOwner: parsedRepoName.Owner, 105 | RepoName: parsedRepoName.Name, 106 | } 107 | } 108 | 109 | alreadyResolvedRepos[repoUniqueKey] = true 110 | 111 | resolvedRepos = append(resolvedRepos, entities.EnvRepository{ 112 | Owner: parsedRepoName.Owner, 113 | ExplicitOwner: parsedRepoName.ExplicitOwner, 114 | 115 | Name: parsedRepoName.Name, 116 | 117 | GitURL: github.BuildGitURL(parsedRepoName), 118 | GitHTTPURL: github.BuildGitHTTPURL(parsedRepoName), 119 | }) 120 | } 121 | 122 | return resolvedRepos, nil 123 | } 124 | 125 | func BuildProtoRepositoriesFromEnv( 126 | env *entities.Env, 127 | ) []*proto.EnvRepository { 128 | 129 | repositories := []*proto.EnvRepository{} 130 | 131 | for _, repository := range env.Repositories { 132 | repositories = append(repositories, &proto.EnvRepository{ 133 | Name: repository.Name, 134 | Owner: repository.Owner, 135 | }) 136 | } 137 | 138 | return repositories 139 | } 140 | -------------------------------------------------------------------------------- /internal/entities/env_served_ports.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/eleven-sh/agent/proto" 7 | "github.com/eleven-sh/cli/internal/agent" 8 | "github.com/eleven-sh/eleven/entities" 9 | ) 10 | 11 | func ReconcileServedPortsState( 12 | env *entities.Env, 13 | agentClientBuilder agent.ClientBuilder, 14 | ) error { 15 | 16 | servedPorts := BuildProtoEnvServedPortsFromEnv(env) 17 | 18 | agentClient := agentClientBuilder.Build( 19 | agent.NewDefaultClientConfig( 20 | []byte(env.SSHKeyPairPEMContent), 21 | env.InstancePublicIPAddress, 22 | ), 23 | ) 24 | 25 | return agentClient.ReconcileServedPortsState( 26 | &proto.ReconcileServedPortsStateRequest{ 27 | ServedPorts: servedPorts, 28 | }, 29 | func(stream agent.ReconcileServedPortsStateStream) error { 30 | 31 | for { 32 | _, err := stream.Recv() 33 | 34 | if err == io.EOF { 35 | break 36 | } 37 | 38 | if err != nil { 39 | return err 40 | } 41 | } 42 | 43 | return nil 44 | }, 45 | ) 46 | } 47 | 48 | func BuildProtoEnvServedPortsFromEnv( 49 | env *entities.Env, 50 | ) map[string]*proto.EnvServedPortBindings { 51 | 52 | portsToServe := map[string]*proto.EnvServedPortBindings{} 53 | 54 | for port, bindings := range env.ServedPorts { 55 | allBindings := []*proto.EnvServedPortBinding{} 56 | 57 | for _, binding := range bindings { 58 | allBindings = append(allBindings, &proto.EnvServedPortBinding{ 59 | Value: binding.Value, 60 | Type: string(binding.Type), 61 | RedirectToHttps: binding.RedirectToHTTPS, 62 | }) 63 | } 64 | 65 | portsToServe[string(port)] = &proto.EnvServedPortBindings{ 66 | Bindings: allBindings, 67 | } 68 | } 69 | 70 | return portsToServe 71 | } 72 | -------------------------------------------------------------------------------- /internal/entities/env_served_ports_test.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/eleven-sh/agent/proto" 8 | "github.com/eleven-sh/eleven/entities" 9 | ) 10 | 11 | func TestBuildProtoEnvServedPortsFromEnv(t *testing.T) { 12 | testCases := []struct { 13 | test string 14 | servedPorts entities.EnvServedPorts 15 | expectedProtoServedPorts map[string]*proto.EnvServedPortBindings 16 | }{ 17 | { 18 | test: "with empty served ports", 19 | 20 | servedPorts: entities.EnvServedPorts{}, 21 | 22 | expectedProtoServedPorts: map[string]*proto.EnvServedPortBindings{}, 23 | }, 24 | 25 | { 26 | test: "with empty served port bindings", 27 | 28 | servedPorts: entities.EnvServedPorts{ 29 | "8000": {}, 30 | }, 31 | 32 | expectedProtoServedPorts: map[string]*proto.EnvServedPortBindings{ 33 | "8000": { 34 | Bindings: []*proto.EnvServedPortBinding{}, 35 | }, 36 | }, 37 | }, 38 | 39 | { 40 | test: "with mixed served ports", 41 | 42 | servedPorts: entities.EnvServedPorts{ 43 | "8000": { 44 | { 45 | Value: "api.eleven.sh", 46 | Type: entities.EnvServedPortBindingTypeDomain, 47 | RedirectToHTTPS: true, 48 | }, 49 | 50 | { 51 | Value: "8000", 52 | Type: entities.EnvServedPortBindingTypePort, 53 | RedirectToHTTPS: false, 54 | }, 55 | }, 56 | 57 | "6000": { 58 | { 59 | Value: "6000", 60 | Type: entities.EnvServedPortBindingTypePort, 61 | RedirectToHTTPS: false, 62 | }, 63 | }, 64 | 65 | "4000": { 66 | { 67 | Value: "test.test.io", 68 | Type: entities.EnvServedPortBindingTypeDomain, 69 | RedirectToHTTPS: true, 70 | }, 71 | }, 72 | }, 73 | 74 | expectedProtoServedPorts: map[string]*proto.EnvServedPortBindings{ 75 | "8000": { 76 | Bindings: []*proto.EnvServedPortBinding{ 77 | { 78 | Type: "domain", 79 | Value: "api.eleven.sh", 80 | RedirectToHttps: true, 81 | }, 82 | 83 | { 84 | Type: "port", 85 | Value: "8000", 86 | RedirectToHttps: false, 87 | }, 88 | }, 89 | }, 90 | 91 | "6000": { 92 | Bindings: []*proto.EnvServedPortBinding{ 93 | { 94 | Type: "port", 95 | Value: "6000", 96 | RedirectToHttps: false, 97 | }, 98 | }, 99 | }, 100 | 101 | "4000": { 102 | Bindings: []*proto.EnvServedPortBinding{ 103 | { 104 | Type: "domain", 105 | Value: "test.test.io", 106 | RedirectToHttps: true, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | } 113 | 114 | for _, tc := range testCases { 115 | t.Run(tc.test, func(t *testing.T) { 116 | env := entities.NewEnv( 117 | "test_env", 118 | 0, 119 | "test_instance_type", 120 | []entities.EnvRepository{}, 121 | entities.EnvRuntimes{}, 122 | ) 123 | 124 | env.ServedPorts = tc.servedPorts 125 | 126 | protoServedPorts := BuildProtoEnvServedPortsFromEnv(env) 127 | 128 | if !reflect.DeepEqual(tc.expectedProtoServedPorts, protoServedPorts) { 129 | t.Fatalf( 130 | "expected proto served ports to equal '%+v', got '%+v'", 131 | tc.expectedProtoServedPorts, 132 | protoServedPorts, 133 | ) 134 | } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /internal/exceptions/cmd.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUserNotLoggedIn = errors.New("ErrUserNotLoggedIn") 7 | ) 8 | 9 | type ErrLoginError struct { 10 | Reason string 11 | } 12 | 13 | func (ErrLoginError) Error() string { 14 | return "ErrLoginError" 15 | } 16 | 17 | type ErrMissingRequirements struct { 18 | MissingRequirements []string 19 | } 20 | 21 | func (ErrMissingRequirements) Error() string { 22 | return "ErrMissingRequirements" 23 | } 24 | 25 | type ErrVSCodeError struct { 26 | Logs string 27 | ErrorMessage string 28 | } 29 | 30 | func (ErrVSCodeError) Error() string { 31 | return "ErrVSCodeError" 32 | } 33 | -------------------------------------------------------------------------------- /internal/features/edit.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "github.com/eleven-sh/agent/config" 5 | "github.com/eleven-sh/cli/internal/interfaces" 6 | "github.com/eleven-sh/eleven/features" 7 | ) 8 | 9 | type EditResponse struct { 10 | Error error 11 | Content EditResponseContent 12 | } 13 | 14 | type EditResponseContent struct { 15 | EnvName string 16 | } 17 | 18 | type EditPresenter interface { 19 | PresentToView(EditResponse) 20 | } 21 | 22 | type EditOutputHandler struct { 23 | presenter EditPresenter 24 | vscodeProcess interfaces.VSCodeProcessManager 25 | vscodeExtensions interfaces.VSCodeExtensionsManager 26 | } 27 | 28 | func NewEditOutputHandler( 29 | presenter EditPresenter, 30 | vscodeProcess interfaces.VSCodeProcessManager, 31 | vscodeExtensions interfaces.VSCodeExtensionsManager, 32 | ) EditOutputHandler { 33 | 34 | return EditOutputHandler{ 35 | presenter: presenter, 36 | vscodeProcess: vscodeProcess, 37 | vscodeExtensions: vscodeExtensions, 38 | } 39 | } 40 | 41 | func (e EditOutputHandler) HandleOutput(output features.EditOutput) error { 42 | stepper := output.Stepper 43 | 44 | handleError := func(err error) error { 45 | stepper.StopCurrentStep() 46 | 47 | e.presenter.PresentToView(EditResponse{ 48 | Error: err, 49 | }) 50 | 51 | return err 52 | } 53 | 54 | if output.Error != nil { 55 | return handleError(output.Error) 56 | } 57 | 58 | stepper.StartTemporaryStepWithoutNewLine( 59 | "Installing Visual Studio Code Remote - SSH extension", 60 | ) 61 | 62 | _, err := e.vscodeExtensions.Install("ms-vscode-remote.remote-ssh") 63 | 64 | if err != nil { 65 | return handleError(err) 66 | } 67 | 68 | stepper.StartTemporaryStepWithoutNewLine( 69 | "Opening Visual Studio Code", 70 | ) 71 | 72 | env := output.Content.Env 73 | 74 | _, err = e.vscodeProcess.OpenOnRemote( 75 | env.LocalSSHConfigHostname, 76 | config.GetVSCodeWorkspaceConfigFilePath(env.Name), 77 | ) 78 | 79 | if err != nil { 80 | return handleError(err) 81 | } 82 | 83 | stepper.StopCurrentStep() 84 | 85 | e.presenter.PresentToView(EditResponse{ 86 | Content: EditResponseContent{ 87 | EnvName: env.Name, 88 | }, 89 | }) 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/features/remove.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "github.com/eleven-sh/eleven/features" 5 | ) 6 | 7 | type RemoveResponse struct { 8 | Error error 9 | Content RemoveResponseContent 10 | } 11 | 12 | type RemoveResponseContent struct { 13 | EnvName string 14 | } 15 | 16 | type RemovePresenter interface { 17 | PresentToView(RemoveResponse) 18 | } 19 | 20 | type RemoveOutputHandler struct { 21 | presenter RemovePresenter 22 | } 23 | 24 | func NewRemoveOutputHandler( 25 | presenter RemovePresenter, 26 | ) RemoveOutputHandler { 27 | 28 | return RemoveOutputHandler{ 29 | presenter: presenter, 30 | } 31 | } 32 | 33 | func (r RemoveOutputHandler) HandleOutput(output features.RemoveOutput) error { 34 | output.Stepper.StopCurrentStep() 35 | 36 | handleError := func(err error) error { 37 | r.presenter.PresentToView(RemoveResponse{ 38 | Error: err, 39 | }) 40 | 41 | return err 42 | } 43 | 44 | if output.Error != nil { 45 | return handleError(output.Error) 46 | } 47 | 48 | env := output.Content.Env 49 | 50 | r.presenter.PresentToView(RemoveResponse{ 51 | Content: RemoveResponseContent{ 52 | EnvName: env.Name, 53 | }, 54 | }) 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/features/serve.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/asaskevich/govalidator" 7 | "github.com/eleven-sh/cli/internal/agent" 8 | cliEntities "github.com/eleven-sh/cli/internal/entities" 9 | "github.com/eleven-sh/cli/internal/hooks" 10 | "github.com/eleven-sh/eleven/entities" 11 | "github.com/eleven-sh/eleven/features" 12 | ) 13 | 14 | type ServeResponse struct { 15 | Error error 16 | Content ServeResponseContent 17 | } 18 | 19 | type ServeResponseContent struct { 20 | EnvName string 21 | EnvPublicIPAddress string 22 | Port string 23 | PortBinding string 24 | } 25 | 26 | type ServePresenter interface { 27 | PresentToView(ServeResponse) 28 | } 29 | 30 | type ServeOutputHandler struct { 31 | presenter ServePresenter 32 | agentClientBuilder agent.ClientBuilder 33 | } 34 | 35 | func NewServeOutputHandler( 36 | presenter ServePresenter, 37 | agentClientBuilder agent.ClientBuilder, 38 | ) ServeOutputHandler { 39 | 40 | return ServeOutputHandler{ 41 | presenter: presenter, 42 | agentClientBuilder: agentClientBuilder, 43 | } 44 | } 45 | 46 | func (s ServeOutputHandler) HandleOutput(output features.ServeOutput) error { 47 | stepper := output.Stepper 48 | 49 | handleError := func(err error) error { 50 | stepper.StopCurrentStep() 51 | 52 | s.presenter.PresentToView(ServeResponse{ 53 | Error: err, 54 | }) 55 | 56 | return err 57 | } 58 | 59 | if output.Error != nil { 60 | return handleError(output.Error) 61 | } 62 | 63 | env := output.Content.Env 64 | 65 | err := cliEntities.ReconcileServedPortsState( 66 | env, 67 | s.agentClientBuilder, 68 | ) 69 | 70 | if err != nil { 71 | return handleError(err) 72 | } 73 | 74 | portBinding := output.Content.PortBinding 75 | 76 | if !govalidator.IsPort(portBinding) { 77 | stepper.StartTemporaryStep("Waiting for Let's Encrypt to issue certificate") 78 | 79 | err := hooks.WaitUntilDomainIsReachableViaHTTPS( 80 | portBinding, 81 | 5*time.Minute, 82 | ) 83 | 84 | if err != nil { 85 | return handleError(entities.ErrLetsEncryptTimedOut{ 86 | Domain: portBinding, 87 | ReturnedError: err, 88 | }) 89 | } 90 | } 91 | 92 | stepper.StopCurrentStep() 93 | 94 | s.presenter.PresentToView(ServeResponse{ 95 | Content: ServeResponseContent{ 96 | EnvName: env.Name, 97 | EnvPublicIPAddress: env.InstancePublicIPAddress, 98 | Port: output.Content.Port, 99 | PortBinding: portBinding, 100 | }, 101 | }) 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/features/uninstall.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/eleven-sh/cli/internal/system" 7 | "github.com/eleven-sh/eleven/features" 8 | ) 9 | 10 | type UninstallResponse struct { 11 | Error error 12 | Content UninstallResponseContent 13 | } 14 | 15 | type UninstallResponseContent struct { 16 | ElevenAlreadyUninstalled bool 17 | SuccessMessage string 18 | AlreadyUninstalledMessage string 19 | ElevenExecutablePath string 20 | ElevenConfigDirPath string 21 | } 22 | 23 | type UninstallPresenter interface { 24 | PresentToView(UninstallResponse) 25 | } 26 | 27 | type UninstallOutputHandler struct { 28 | presenter UninstallPresenter 29 | } 30 | 31 | func NewUninstallOutputHandler( 32 | presenter UninstallPresenter, 33 | ) UninstallOutputHandler { 34 | 35 | return UninstallOutputHandler{ 36 | presenter: presenter, 37 | } 38 | } 39 | 40 | func (u UninstallOutputHandler) HandleOutput(output features.UninstallOutput) error { 41 | output.Stepper.StopCurrentStep() 42 | 43 | handleError := func(err error) error { 44 | u.presenter.PresentToView(UninstallResponse{ 45 | Error: err, 46 | }) 47 | 48 | return err 49 | } 50 | 51 | if output.Error != nil { 52 | return handleError(output.Error) 53 | } 54 | 55 | elevenExecutablePath, err := os.Executable() 56 | 57 | if err != nil { 58 | elevenExecutablePath = "" 59 | } 60 | 61 | elevenConfigDirPath := system.UserConfigDir() 62 | 63 | u.presenter.PresentToView(UninstallResponse{ 64 | Content: UninstallResponseContent{ 65 | ElevenAlreadyUninstalled: output.Content.ElevenAlreadyUninstalled, 66 | SuccessMessage: output.Content.SuccessMessage, 67 | AlreadyUninstalledMessage: output.Content.AlreadyUninstalledMessage, 68 | ElevenExecutablePath: elevenExecutablePath, 69 | ElevenConfigDirPath: elevenConfigDirPath, 70 | }, 71 | }) 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/features/unserve.go: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/agent" 5 | "github.com/eleven-sh/cli/internal/entities" 6 | "github.com/eleven-sh/eleven/features" 7 | ) 8 | 9 | type UnserveResponse struct { 10 | Error error 11 | Content UnserveResponseContent 12 | } 13 | 14 | type UnserveResponseContent struct { 15 | EnvName string 16 | Port string 17 | } 18 | 19 | type UnservePresenter interface { 20 | PresentToView(UnserveResponse) 21 | } 22 | 23 | type UnserveOutputHandler struct { 24 | presenter UnservePresenter 25 | agentClientBuilder agent.ClientBuilder 26 | } 27 | 28 | func NewUnserveOutputHandler( 29 | presenter UnservePresenter, 30 | agentClientBuilder agent.ClientBuilder, 31 | ) UnserveOutputHandler { 32 | 33 | return UnserveOutputHandler{ 34 | presenter: presenter, 35 | agentClientBuilder: agentClientBuilder, 36 | } 37 | } 38 | 39 | func (u UnserveOutputHandler) HandleOutput(output features.UnserveOutput) error { 40 | stepper := output.Stepper 41 | 42 | handleError := func(err error) error { 43 | stepper.StopCurrentStep() 44 | 45 | u.presenter.PresentToView(UnserveResponse{ 46 | Error: err, 47 | }) 48 | 49 | return err 50 | } 51 | 52 | if output.Error != nil { 53 | return handleError(output.Error) 54 | } 55 | 56 | env := output.Content.Env 57 | 58 | err := entities.ReconcileServedPortsState( 59 | env, 60 | u.agentClientBuilder, 61 | ) 62 | 63 | if err != nil { 64 | return handleError(err) 65 | } 66 | 67 | stepper.StopCurrentStep() 68 | 69 | u.presenter.PresentToView(UnserveResponse{ 70 | Content: UnserveResponseContent{ 71 | EnvName: env.Name, 72 | Port: output.Content.Port, 73 | }, 74 | }) 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/globals/values.go: -------------------------------------------------------------------------------- 1 | package globals 2 | 3 | type CloudProvider string 4 | 5 | const ( 6 | AWSCloudProvider CloudProvider = "aws" 7 | HetznerCloudProvider CloudProvider = "hetzner" 8 | ) 9 | 10 | var ( 11 | CurrentCloudProvider CloudProvider 12 | CurrentCloudProviderArgs string 13 | ) 14 | -------------------------------------------------------------------------------- /internal/hooks/domain_reachability_checker.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/eleven-sh/agent/proto" 10 | "github.com/eleven-sh/cli/internal/agent" 11 | cliEntities "github.com/eleven-sh/cli/internal/entities" 12 | "github.com/eleven-sh/eleven/entities" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | type DomainReachabilityChecker struct { 17 | agentClientBuilder agent.ClientBuilder 18 | } 19 | 20 | func NewDomainReachabilityChecker( 21 | agentClientBuilder agent.ClientBuilder, 22 | ) DomainReachabilityChecker { 23 | 24 | return DomainReachabilityChecker{ 25 | agentClientBuilder: agentClientBuilder, 26 | } 27 | } 28 | 29 | func (d DomainReachabilityChecker) Check( 30 | env *entities.Env, 31 | domain string, 32 | ) (reachable bool, redirToHTTPS bool, returnedError error) { 33 | 34 | agentClient := d.agentClientBuilder.Build( 35 | agent.NewDefaultClientConfig( 36 | []byte(env.SSHKeyPairPEMContent), 37 | env.InstancePublicIPAddress, 38 | ), 39 | ) 40 | 41 | uniqueID := uuid.NewString() 42 | servedPorts := cliEntities.BuildProtoEnvServedPortsFromEnv(env) 43 | 44 | err := agentClient.CheckDomainReachability(&proto.CheckDomainReachabilityRequest{ 45 | Domain: domain, 46 | ServedPorts: servedPorts, 47 | UniqueId: uniqueID, 48 | }, func(stream agent.CheckDomainReachabilityStream) error { 49 | _, err := stream.Recv() 50 | 51 | if err == io.EOF { 52 | return nil 53 | } 54 | 55 | return err 56 | }) 57 | 58 | if err != nil { 59 | returnedError = err 60 | return 61 | } 62 | 63 | pollTimeoutChan := time.After(20 * time.Second) 64 | pollSleepDuration := 1 * time.Second 65 | 66 | for { 67 | select { 68 | case <-pollTimeoutChan: 69 | return 70 | default: 71 | redirectedToHTTPS := false 72 | 73 | httpClient := &http.Client{ 74 | Timeout: 4 * time.Second, 75 | CheckRedirect: func(nextReq *http.Request, prevReq []*http.Request) error { 76 | 77 | if nextReq.URL.Scheme == "https" && 78 | nextReq.URL.Hostname() == domain { 79 | 80 | redirectedToHTTPS = true 81 | } 82 | 83 | return nil 84 | }, 85 | } 86 | 87 | httpResp, err := httpClient.Get("http://" + domain) 88 | 89 | if err != nil { 90 | returnedError = nil 91 | break 92 | } 93 | 94 | if httpResp.StatusCode != 200 { 95 | 96 | if redirectedToHTTPS { // proxy 97 | 98 | if httpResp.StatusCode >= 520 && httpResp.StatusCode <= 527 { // Cloudflare 99 | 100 | // Cloudflare tries to connect through 101 | // HTTPS (SSL/TLS encryption mode is set to "Full") 102 | 103 | returnedError = entities.ErrCloudflareSSLFull{ 104 | Domain: domain, 105 | } 106 | return 107 | } 108 | 109 | returnedError = entities.ErrProxyForceHTTPS{ 110 | Domain: domain, 111 | } 112 | return 113 | } 114 | 115 | returnedError = nil 116 | break 117 | } 118 | 119 | httpBody, err := io.ReadAll(httpResp.Body) 120 | httpResp.Body.Close() 121 | 122 | if err != nil { 123 | returnedError = err 124 | break 125 | } 126 | 127 | returnedError = nil 128 | 129 | if string(httpBody) == uniqueID { 130 | 131 | reachable = true 132 | 133 | // We want to check that we don't have a proxy 134 | // in front of the sandbox that passes 135 | // all requests as HTTP (like Cloudflare with SSL mode set to "flexible") 136 | httpsClient := &http.Client{ 137 | Timeout: 4 * time.Second, 138 | } 139 | 140 | httpsResp, err := httpsClient.Get("https://" + domain) 141 | 142 | if err == nil && httpsResp.StatusCode == 200 { 143 | httpsBody, err := io.ReadAll(httpsResp.Body) 144 | httpsResp.Body.Close() 145 | 146 | if err == nil && string(httpsBody) == uniqueID { 147 | // OK. We have a proxy in front of the sandbox that passes 148 | // all requests as HTTP (like Cloudflare with SSL mode set to "flexible"). 149 | // If we redirect HTTP to HTTPS at the server level, 150 | // we will get a redirect loop. 151 | redirToHTTPS = false 152 | return 153 | } 154 | } 155 | 156 | redirToHTTPS = true 157 | return 158 | } 159 | } // <- end of select 160 | 161 | time.Sleep(pollSleepDuration) 162 | } 163 | } 164 | 165 | func WaitUntilDomainIsReachableViaHTTPS( 166 | domain string, 167 | timeout time.Duration, 168 | ) (returnedError error) { 169 | 170 | pollTimeoutChan := time.After(timeout) 171 | pollSleepDuration := 1 * time.Second 172 | 173 | for { 174 | select { 175 | case <-pollTimeoutChan: 176 | return 177 | default: 178 | httpsClient := &http.Client{ 179 | Timeout: 4 * time.Second, 180 | } 181 | 182 | httpsResp, err := httpsClient.Get("https://" + domain) 183 | 184 | if err != nil { 185 | returnedError = err 186 | break 187 | } 188 | 189 | if httpsResp.StatusCode >= 520 && httpsResp.StatusCode <= 527 { 190 | returnedError = fmt.Errorf( 191 | "cloudflare cannot reach origin (HTTP status code %d)", 192 | httpsResp.StatusCode, 193 | ) 194 | break 195 | } 196 | 197 | returnedError = nil 198 | return 199 | } // <- end of select 200 | 201 | time.Sleep(pollSleepDuration) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /internal/hooks/pre_remove.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/eleven-sh/cli/internal/config" 7 | cliEntities "github.com/eleven-sh/cli/internal/entities" 8 | "github.com/eleven-sh/cli/internal/interfaces" 9 | "github.com/eleven-sh/eleven/entities" 10 | ) 11 | 12 | type PreRemove struct { 13 | sshConfig interfaces.SSHConfigManager 14 | sshKeys interfaces.SSHKeysManager 15 | sshKnownHosts interfaces.SSHKnownHostsManager 16 | userConfig interfaces.UserConfigManager 17 | github interfaces.GitHubManager 18 | } 19 | 20 | func NewPreRemove( 21 | sshConfig interfaces.SSHConfigManager, 22 | sshKeys interfaces.SSHKeysManager, 23 | sshKnownHosts interfaces.SSHKnownHostsManager, 24 | userConfig interfaces.UserConfigManager, 25 | github interfaces.GitHubManager, 26 | ) PreRemove { 27 | 28 | return PreRemove{ 29 | sshConfig: sshConfig, 30 | sshKeys: sshKeys, 31 | sshKnownHosts: sshKnownHosts, 32 | userConfig: userConfig, 33 | github: github, 34 | } 35 | } 36 | 37 | func (p PreRemove) Run( 38 | cloudService entities.CloudService, 39 | elevenConfig *entities.Config, 40 | cluster *entities.Cluster, 41 | env *entities.Env, 42 | ) error { 43 | 44 | err := p.sshKeys.RemovePEMIfExists(env.GetSSHKeyPairName()) 45 | 46 | if err != nil { 47 | return err 48 | } 49 | 50 | err = p.sshConfig.RemoveHostIfExists(env.LocalSSHConfigHostname) 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | sshHostname := env.InstancePublicIPAddress 57 | err = p.sshKnownHosts.RemoveIfExists(sshHostname) 58 | 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // User could remove dev env in creating state 64 | // (in case of error for example) 65 | if len(env.AdditionalPropertiesJSON) == 0 { 66 | return nil 67 | } 68 | 69 | var envAdditionalProperties *cliEntities.EnvAdditionalProperties 70 | err = json.Unmarshal( 71 | []byte(env.AdditionalPropertiesJSON), 72 | &envAdditionalProperties, 73 | ) 74 | 75 | if err != nil { 76 | return err 77 | } 78 | 79 | githubAccessToken := p.userConfig.GetString( 80 | config.UserConfigKeyGitHubAccessToken, 81 | ) 82 | 83 | if envAdditionalProperties.GitHubCreatedSSHKeyId != nil { 84 | err = p.github.RemoveSSHKey( 85 | githubAccessToken, 86 | *envAdditionalProperties.GitHubCreatedSSHKeyId, 87 | ) 88 | 89 | if err != nil && !p.github.IsNotFoundError(err) { 90 | return err 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/interfaces/browser.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | type BrowserManager interface { 4 | OpenURL(url string) error 5 | } 6 | -------------------------------------------------------------------------------- /internal/interfaces/github.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | cliGitHub "github.com/eleven-sh/eleven/github" 5 | "github.com/google/go-github/v43/github" 6 | ) 7 | 8 | type GitHubManager interface { 9 | GetAuthenticatedUser(accessToken string) (*cliGitHub.AuthenticatedUser, error) 10 | DoesRepositoryExist(accessToken, repositoryOwner, repositoryName string) (bool, error) 11 | 12 | CreateSSHKey(accessToken, keyPairName, publicKeyContent string) (*github.Key, error) 13 | RemoveSSHKey(accessToken string, sshKeyID int64) error 14 | 15 | IsNotFoundError(err error) bool 16 | } 17 | -------------------------------------------------------------------------------- /internal/interfaces/logger.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | type Logger interface { 4 | Info(format string, v ...interface{}) 5 | Warning(format string, v ...interface{}) 6 | Error(format string, v ...interface{}) 7 | Log(format string, v ...interface{}) 8 | LogNoNewline(format string, v ...interface{}) 9 | Write(p []byte) (n int, err error) 10 | } 11 | -------------------------------------------------------------------------------- /internal/interfaces/sleeper.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "time" 4 | 5 | type Sleeper interface { 6 | Sleep(d time.Duration) 7 | } 8 | -------------------------------------------------------------------------------- /internal/interfaces/ssh.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // SSH known hosts 4 | 5 | type SSHKnownHostsManager interface { 6 | RemoveIfExists(hostname string) error 7 | AddOrReplace(hostname, algorithm, fingerprint string) error 8 | } 9 | 10 | // SSH Config 11 | 12 | type SSHConfigManager interface { 13 | AddOrReplaceHost(hostKey, hostName, identityFile, user string, port int64) error 14 | UpdateHost(hostKey string, hostName, identityFile, user *string) error 15 | RemoveHostIfExists(hostKey string) error 16 | CountEntriesWithHostPrefix(hostPrefix string) (int, error) 17 | } 18 | 19 | // SSH keys 20 | 21 | type SSHKeysManager interface { 22 | CreateOrReplacePEM(PEMName, PEMContent string) (string, error) 23 | RemovePEMIfExists(PEMPath string) error 24 | GetPEMFilePath(PEMName string) string 25 | } 26 | -------------------------------------------------------------------------------- /internal/interfaces/user_config.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/config" 5 | "github.com/eleven-sh/eleven/github" 6 | ) 7 | 8 | type UserConfigManager interface { 9 | GetString(key config.UserConfigKey) string 10 | GetBool(key config.UserConfigKey) bool 11 | Set(key config.UserConfigKey, value interface{}) 12 | WriteConfig() error 13 | PopulateFromGitHubUser(githubUser *github.AuthenticatedUser) 14 | } 15 | -------------------------------------------------------------------------------- /internal/interfaces/vscode.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | type VSCodeProcessManager interface { 4 | OpenOnRemote(hostKey, pathToOpen string) (cmdOutput string, cmdError error) 5 | } 6 | 7 | type VSCodeExtensionsManager interface { 8 | Install(extensionName string) (cmdOutput string, cmdError error) 9 | } 10 | -------------------------------------------------------------------------------- /internal/mocks/aws_user_config_env_vars_resolver.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/eleven-sh/cli/internal/cloudproviders/aws (interfaces: UserConfigEnvVarsResolver) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | userconfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // AWSUserConfigEnvVarsResolver is a mock of UserConfigEnvVarsResolver interface. 15 | type AWSUserConfigEnvVarsResolver struct { 16 | ctrl *gomock.Controller 17 | recorder *AWSUserConfigEnvVarsResolverMockRecorder 18 | } 19 | 20 | // AWSUserConfigEnvVarsResolverMockRecorder is the mock recorder for AWSUserConfigEnvVarsResolver. 21 | type AWSUserConfigEnvVarsResolverMockRecorder struct { 22 | mock *AWSUserConfigEnvVarsResolver 23 | } 24 | 25 | // NewAWSUserConfigEnvVarsResolver creates a new mock instance. 26 | func NewAWSUserConfigEnvVarsResolver(ctrl *gomock.Controller) *AWSUserConfigEnvVarsResolver { 27 | mock := &AWSUserConfigEnvVarsResolver{ctrl: ctrl} 28 | mock.recorder = &AWSUserConfigEnvVarsResolverMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *AWSUserConfigEnvVarsResolver) EXPECT() *AWSUserConfigEnvVarsResolverMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Resolve mocks base method. 38 | func (m *AWSUserConfigEnvVarsResolver) Resolve() (*userconfig.Config, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Resolve") 41 | ret0, _ := ret[0].(*userconfig.Config) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Resolve indicates an expected call of Resolve. 47 | func (mr *AWSUserConfigEnvVarsResolverMockRecorder) Resolve() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*AWSUserConfigEnvVarsResolver)(nil).Resolve)) 50 | } 51 | -------------------------------------------------------------------------------- /internal/mocks/aws_user_config_files_resolver.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/eleven-sh/cli/internal/cloudproviders/aws (interfaces: UserConfigFilesResolver) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | userconfig "github.com/eleven-sh/aws-cloud-provider/userconfig" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // AWSUserConfigFilesResolver is a mock of UserConfigFilesResolver interface. 15 | type AWSUserConfigFilesResolver struct { 16 | ctrl *gomock.Controller 17 | recorder *AWSUserConfigFilesResolverMockRecorder 18 | } 19 | 20 | // AWSUserConfigFilesResolverMockRecorder is the mock recorder for AWSUserConfigFilesResolver. 21 | type AWSUserConfigFilesResolverMockRecorder struct { 22 | mock *AWSUserConfigFilesResolver 23 | } 24 | 25 | // NewAWSUserConfigFilesResolver creates a new mock instance. 26 | func NewAWSUserConfigFilesResolver(ctrl *gomock.Controller) *AWSUserConfigFilesResolver { 27 | mock := &AWSUserConfigFilesResolver{ctrl: ctrl} 28 | mock.recorder = &AWSUserConfigFilesResolverMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *AWSUserConfigFilesResolver) EXPECT() *AWSUserConfigFilesResolverMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Resolve mocks base method. 38 | func (m *AWSUserConfigFilesResolver) Resolve() (*userconfig.Config, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Resolve") 41 | ret0, _ := ret[0].(*userconfig.Config) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Resolve indicates an expected call of Resolve. 47 | func (mr *AWSUserConfigFilesResolverMockRecorder) Resolve() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*AWSUserConfigFilesResolver)(nil).Resolve)) 50 | } 51 | -------------------------------------------------------------------------------- /internal/mocks/hetzner_user_config_env_vars_resolver.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/eleven-sh/cli/internal/cloudproviders/hetzner (interfaces: UserConfigEnvVarsResolver) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | userconfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // HetznerUserConfigEnvVarsResolver is a mock of UserConfigEnvVarsResolver interface. 15 | type HetznerUserConfigEnvVarsResolver struct { 16 | ctrl *gomock.Controller 17 | recorder *HetznerUserConfigEnvVarsResolverMockRecorder 18 | } 19 | 20 | // HetznerUserConfigEnvVarsResolverMockRecorder is the mock recorder for HetznerUserConfigEnvVarsResolver. 21 | type HetznerUserConfigEnvVarsResolverMockRecorder struct { 22 | mock *HetznerUserConfigEnvVarsResolver 23 | } 24 | 25 | // NewHetznerUserConfigEnvVarsResolver creates a new mock instance. 26 | func NewHetznerUserConfigEnvVarsResolver(ctrl *gomock.Controller) *HetznerUserConfigEnvVarsResolver { 27 | mock := &HetznerUserConfigEnvVarsResolver{ctrl: ctrl} 28 | mock.recorder = &HetznerUserConfigEnvVarsResolverMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *HetznerUserConfigEnvVarsResolver) EXPECT() *HetznerUserConfigEnvVarsResolverMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Resolve mocks base method. 38 | func (m *HetznerUserConfigEnvVarsResolver) Resolve() (*userconfig.Config, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Resolve") 41 | ret0, _ := ret[0].(*userconfig.Config) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Resolve indicates an expected call of Resolve. 47 | func (mr *HetznerUserConfigEnvVarsResolverMockRecorder) Resolve() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*HetznerUserConfigEnvVarsResolver)(nil).Resolve)) 50 | } 51 | -------------------------------------------------------------------------------- /internal/mocks/hetzner_user_config_files_resolver.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/eleven-sh/cli/internal/cloudproviders/hetzner (interfaces: UserConfigFilesResolver) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | userconfig "github.com/eleven-sh/hetzner-cloud-provider/userconfig" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // HetznerUserConfigFilesResolver is a mock of UserConfigFilesResolver interface. 15 | type HetznerUserConfigFilesResolver struct { 16 | ctrl *gomock.Controller 17 | recorder *HetznerUserConfigFilesResolverMockRecorder 18 | } 19 | 20 | // HetznerUserConfigFilesResolverMockRecorder is the mock recorder for HetznerUserConfigFilesResolver. 21 | type HetznerUserConfigFilesResolverMockRecorder struct { 22 | mock *HetznerUserConfigFilesResolver 23 | } 24 | 25 | // NewHetznerUserConfigFilesResolver creates a new mock instance. 26 | func NewHetznerUserConfigFilesResolver(ctrl *gomock.Controller) *HetznerUserConfigFilesResolver { 27 | mock := &HetznerUserConfigFilesResolver{ctrl: ctrl} 28 | mock.recorder = &HetznerUserConfigFilesResolverMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *HetznerUserConfigFilesResolver) EXPECT() *HetznerUserConfigFilesResolverMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Resolve mocks base method. 38 | func (m *HetznerUserConfigFilesResolver) Resolve() (*userconfig.Config, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Resolve") 41 | ret0, _ := ret[0].(*userconfig.Config) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Resolve indicates an expected call of Resolve. 47 | func (mr *HetznerUserConfigFilesResolverMockRecorder) Resolve() *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*HetznerUserConfigFilesResolver)(nil).Resolve)) 50 | } 51 | -------------------------------------------------------------------------------- /internal/mocks/presenters_init.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/eleven-sh/cli/internal/presenters (interfaces: InitViewer) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | views "github.com/eleven-sh/cli/internal/views" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // PresentersInitViewer is a mock of InitViewer interface. 15 | type PresentersInitViewer struct { 16 | ctrl *gomock.Controller 17 | recorder *PresentersInitViewerMockRecorder 18 | } 19 | 20 | // PresentersInitViewerMockRecorder is the mock recorder for PresentersInitViewer. 21 | type PresentersInitViewerMockRecorder struct { 22 | mock *PresentersInitViewer 23 | } 24 | 25 | // NewPresentersInitViewer creates a new mock instance. 26 | func NewPresentersInitViewer(ctrl *gomock.Controller) *PresentersInitViewer { 27 | mock := &PresentersInitViewer{ctrl: ctrl} 28 | mock.recorder = &PresentersInitViewerMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *PresentersInitViewer) EXPECT() *PresentersInitViewerMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // View mocks base method. 38 | func (m *PresentersInitViewer) View(arg0 views.InitViewData) { 39 | m.ctrl.T.Helper() 40 | m.ctrl.Call(m, "View", arg0) 41 | } 42 | 43 | // View indicates an expected call of View. 44 | func (mr *PresentersInitViewerMockRecorder) View(arg0 interface{}) *gomock.Call { 45 | mr.mock.ctrl.T.Helper() 46 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "View", reflect.TypeOf((*PresentersInitViewer)(nil).View), arg0) 47 | } 48 | -------------------------------------------------------------------------------- /internal/mocks/views_displayer.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/eleven-sh/cli/internal/views (interfaces: Displayer) 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | io "io" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockDisplayer is a mock of Displayer interface. 15 | type MockDisplayer struct { 16 | ctrl *gomock.Controller 17 | recorder *MockDisplayerMockRecorder 18 | } 19 | 20 | // MockDisplayerMockRecorder is the mock recorder for MockDisplayer. 21 | type MockDisplayerMockRecorder struct { 22 | mock *MockDisplayer 23 | } 24 | 25 | // NewMockDisplayer creates a new mock instance. 26 | func NewMockDisplayer(ctrl *gomock.Controller) *MockDisplayer { 27 | mock := &MockDisplayer{ctrl: ctrl} 28 | mock.recorder = &MockDisplayerMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockDisplayer) EXPECT() *MockDisplayerMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Display mocks base method. 38 | func (m *MockDisplayer) Display(arg0 io.Writer, arg1 string, arg2 ...interface{}) { 39 | m.ctrl.T.Helper() 40 | varargs := []interface{}{arg0, arg1} 41 | for _, a := range arg2 { 42 | varargs = append(varargs, a) 43 | } 44 | m.ctrl.Call(m, "Display", varargs...) 45 | } 46 | 47 | // Display indicates an expected call of Display. 48 | func (mr *MockDisplayerMockRecorder) Display(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | varargs := append([]interface{}{arg0, arg1}, arg2...) 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Display", reflect.TypeOf((*MockDisplayer)(nil).Display), varargs...) 52 | } 53 | -------------------------------------------------------------------------------- /internal/presenters/edit.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/features" 5 | "github.com/eleven-sh/cli/internal/views" 6 | ) 7 | 8 | type EditViewer interface { 9 | View(views.EditViewData) 10 | } 11 | 12 | type EditPresenter struct { 13 | viewableErrorBuilder ViewableErrorBuilder 14 | viewer EditViewer 15 | } 16 | 17 | func NewEditPresenter( 18 | viewableErrorBuilder ViewableErrorBuilder, 19 | viewer EditViewer, 20 | ) EditPresenter { 21 | 22 | return EditPresenter{ 23 | viewableErrorBuilder: viewableErrorBuilder, 24 | viewer: viewer, 25 | } 26 | } 27 | 28 | func (e EditPresenter) PresentToView(response features.EditResponse) { 29 | viewData := views.EditViewData{} 30 | 31 | if response.Error == nil { 32 | viewData.Content = views.EditViewDataContent{ 33 | Message: "Your editor is now open and connected to your sandbox.", 34 | } 35 | 36 | e.viewer.View(viewData) 37 | return 38 | } 39 | 40 | viewData.Error = e.viewableErrorBuilder.Build(response.Error) 41 | e.viewer.View(viewData) 42 | } 43 | -------------------------------------------------------------------------------- /internal/presenters/init.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eleven-sh/cli/internal/config" 7 | "github.com/eleven-sh/cli/internal/features" 8 | "github.com/eleven-sh/cli/internal/globals" 9 | "github.com/eleven-sh/cli/internal/views" 10 | ) 11 | 12 | //go:generate go run github.com/golang/mock/mockgen -destination=../mocks/presenters_init.go -package=mocks -mock_names InitViewer=PresentersInitViewer github.com/eleven-sh/cli/internal/presenters InitViewer 13 | type InitViewer interface { 14 | View(views.InitViewData) 15 | } 16 | 17 | type InitPresenter struct { 18 | viewableErrorBuilder ViewableErrorBuilder 19 | viewer InitViewer 20 | } 21 | 22 | func NewInitPresenter( 23 | viewableErrorBuilder ViewableErrorBuilder, 24 | viewer InitViewer, 25 | ) InitPresenter { 26 | 27 | return InitPresenter{ 28 | viewableErrorBuilder: viewableErrorBuilder, 29 | viewer: viewer, 30 | } 31 | } 32 | 33 | func (i InitPresenter) PresentToView(response features.InitResponse) { 34 | viewData := views.InitViewData{} 35 | 36 | if response.Error == nil { 37 | envName := response.Content.EnvName 38 | envAlreadyCreated := response.Content.EnvAlreadyCreated 39 | 40 | viewDataMessage := "The sandbox \"" + envName + "\" was initialized." 41 | if envAlreadyCreated { 42 | viewDataMessage = "The sandbox \"" + envName + "\" is already initialized." 43 | } 44 | 45 | currentCloudProviderCmd := string(globals.CurrentCloudProvider) 46 | currentCloudProviderCmdArgs := globals.CurrentCloudProviderArgs 47 | 48 | if len(currentCloudProviderCmdArgs) > 0 { 49 | currentCloudProviderCmd += " " + currentCloudProviderCmdArgs 50 | } 51 | 52 | bold := config.ColorsBold 53 | 54 | viewDataSubtext := fmt.Sprintf( 55 | "The public IP of your sandbox is: %s\n\n"+ 56 | "To connect to your sandbox:\n\n"+ 57 | " - With your editor: `%s`\n\n"+ 58 | " - With SSH : `%s`\n\n"+ 59 | "To allow TCP traffic on a port: `%s`", 60 | bold(response.Content.EnvPublicIPAddress), 61 | bold(config.ColorsBlue("eleven "+currentCloudProviderCmd+" edit "+envName)), 62 | bold(config.ColorsBlue("ssh "+response.Content.EnvLocalSSHConfigHostname)), 63 | bold(config.ColorsBlue("eleven "+currentCloudProviderCmd+" serve "+envName+" [--as ]")), 64 | ) 65 | 66 | runtimes := response.Content.EnvRuntimes 67 | runtimesAsText := "" 68 | 69 | for runtimeName, runtimeVersion := range runtimes { 70 | runtimesAsText += config.ColorsBGBlue( 71 | fmt.Sprintf(" %s@%s ", runtimeName, runtimeVersion), 72 | ) + " " 73 | } 74 | 75 | if len(runtimesAsText) > 0 { 76 | viewDataSubtext += "\n\n" 77 | viewDataSubtext += "Installed runtimes: " + bold(config.ColorsWhite(runtimesAsText)) 78 | } 79 | 80 | viewData.Content = views.InitViewDataContent{ 81 | ShowAsWarning: envAlreadyCreated, 82 | Message: viewDataMessage, 83 | Subtext: viewDataSubtext, 84 | } 85 | 86 | i.viewer.View(viewData) 87 | return 88 | } 89 | 90 | viewData.Error = i.viewableErrorBuilder.Build(response.Error) 91 | i.viewer.View(viewData) 92 | } 93 | -------------------------------------------------------------------------------- /internal/presenters/login.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/features" 5 | "github.com/eleven-sh/cli/internal/views" 6 | ) 7 | 8 | type LoginViewer interface { 9 | View(views.LoginViewData) 10 | } 11 | 12 | type LoginPresenter struct { 13 | viewableErrorBuilder ViewableErrorBuilder 14 | viewer LoginViewer 15 | } 16 | 17 | func NewLoginPresenter( 18 | viewableErrorBuilder ViewableErrorBuilder, 19 | viewer LoginViewer, 20 | ) LoginPresenter { 21 | 22 | return LoginPresenter{ 23 | viewableErrorBuilder: viewableErrorBuilder, 24 | viewer: viewer, 25 | } 26 | } 27 | 28 | func (l LoginPresenter) PresentToView(response features.LoginResponse) { 29 | viewData := views.LoginViewData{} 30 | 31 | if response.Error == nil { 32 | viewData.Content = views.LoginViewDataContent{ 33 | Message: "Your GitHub account is now connected.", 34 | } 35 | 36 | l.viewer.View(viewData) 37 | return 38 | } 39 | 40 | viewData.Error = l.viewableErrorBuilder.Build(response.Error) 41 | l.viewer.View(viewData) 42 | } 43 | -------------------------------------------------------------------------------- /internal/presenters/remove.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "github.com/eleven-sh/cli/internal/features" 5 | "github.com/eleven-sh/cli/internal/views" 6 | ) 7 | 8 | type RemoveViewer interface { 9 | View(views.RemoveViewData) 10 | } 11 | 12 | type RemovePresenter struct { 13 | viewableErrorBuilder ViewableErrorBuilder 14 | viewer RemoveViewer 15 | } 16 | 17 | func NewRemovePresenter( 18 | viewableErrorBuilder ViewableErrorBuilder, 19 | viewer RemoveViewer, 20 | ) RemovePresenter { 21 | 22 | return RemovePresenter{ 23 | viewableErrorBuilder: viewableErrorBuilder, 24 | viewer: viewer, 25 | } 26 | } 27 | 28 | func (r RemovePresenter) PresentToView(response features.RemoveResponse) { 29 | viewData := views.RemoveViewData{} 30 | 31 | if response.Error == nil { 32 | envName := response.Content.EnvName 33 | 34 | viewData.Content = views.RemoveViewDataContent{ 35 | Message: "The sandbox \"" + envName + "\" was removed.", 36 | } 37 | 38 | r.viewer.View(viewData) 39 | return 40 | } 41 | 42 | viewData.Error = r.viewableErrorBuilder.Build(response.Error) 43 | r.viewer.View(viewData) 44 | } 45 | -------------------------------------------------------------------------------- /internal/presenters/serve.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/asaskevich/govalidator" 7 | "github.com/eleven-sh/cli/internal/config" 8 | "github.com/eleven-sh/cli/internal/features" 9 | "github.com/eleven-sh/cli/internal/views" 10 | ) 11 | 12 | type ServeViewer interface { 13 | View(views.ServeViewData) 14 | } 15 | 16 | type ServePresenter struct { 17 | viewableErrorBuilder ViewableErrorBuilder 18 | viewer ServeViewer 19 | } 20 | 21 | func NewServePresenter( 22 | viewableErrorBuilder ViewableErrorBuilder, 23 | viewer ServeViewer, 24 | ) ServePresenter { 25 | 26 | return ServePresenter{ 27 | viewableErrorBuilder: viewableErrorBuilder, 28 | viewer: viewer, 29 | } 30 | } 31 | 32 | func (s ServePresenter) PresentToView(response features.ServeResponse) { 33 | viewData := views.ServeViewData{} 34 | 35 | if response.Error == nil { 36 | portBinding := response.Content.PortBinding 37 | 38 | envIPAddress := response.Content.EnvPublicIPAddress 39 | servedPort := response.Content.Port 40 | 41 | portReachableAt := envIPAddress + ":" + portBinding 42 | if !govalidator.IsPort(portBinding) { 43 | portReachableAt = "https://" + portBinding 44 | } 45 | 46 | viewDataMessage := fmt.Sprintf( 47 | "The port \"%s\" is now reachable at: %s", 48 | servedPort, 49 | config.ColorsBlue(portReachableAt), 50 | ) 51 | 52 | viewData.Content = views.ServeViewDataContent{ 53 | Message: viewDataMessage, 54 | } 55 | 56 | s.viewer.View(viewData) 57 | return 58 | } 59 | 60 | viewData.Error = s.viewableErrorBuilder.Build(response.Error) 61 | s.viewer.View(viewData) 62 | } 63 | -------------------------------------------------------------------------------- /internal/presenters/uninstall.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eleven-sh/cli/internal/config" 7 | "github.com/eleven-sh/cli/internal/features" 8 | "github.com/eleven-sh/cli/internal/views" 9 | ) 10 | 11 | type UninstallViewer interface { 12 | View(views.UninstallViewData) 13 | } 14 | 15 | type UninstallPresenter struct { 16 | viewableErrorBuilder ViewableErrorBuilder 17 | viewer UninstallViewer 18 | } 19 | 20 | func NewUninstallPresenter( 21 | viewableErrorBuilder ViewableErrorBuilder, 22 | viewer UninstallViewer, 23 | ) UninstallPresenter { 24 | 25 | return UninstallPresenter{ 26 | viewableErrorBuilder: viewableErrorBuilder, 27 | viewer: viewer, 28 | } 29 | } 30 | 31 | func (u UninstallPresenter) PresentToView(response features.UninstallResponse) { 32 | viewData := views.UninstallViewData{} 33 | 34 | if response.Error == nil { 35 | bold := config.ColorsBold 36 | 37 | elevenAlreadyUninstalled := response.Content.ElevenAlreadyUninstalled 38 | 39 | viewDataMessage := response.Content.SuccessMessage 40 | if elevenAlreadyUninstalled { 41 | viewDataMessage = response.Content.AlreadyUninstalledMessage 42 | } 43 | 44 | viewDataSubtext := fmt.Sprintf( 45 | "If you want to remove Eleven entirely:\n\n"+ 46 | " - Remove the Eleven CLI (located at %s)\n\n"+ 47 | " - Remove the Eleven configuration (located at %s)\n\n"+ 48 | " - Unauthorize the Eleven application on GitHub by going to: %s", 49 | bold(response.Content.ElevenExecutablePath), 50 | bold(response.Content.ElevenConfigDirPath), 51 | bold("https://github.com/settings/applications"), 52 | ) 53 | 54 | viewData.Content = views.UninstallViewDataContent{ 55 | ShowAsWarning: elevenAlreadyUninstalled, 56 | Message: viewDataMessage, 57 | Subtext: viewDataSubtext, 58 | } 59 | 60 | u.viewer.View(viewData) 61 | return 62 | } 63 | 64 | viewData.Error = u.viewableErrorBuilder.Build(response.Error) 65 | u.viewer.View(viewData) 66 | } 67 | -------------------------------------------------------------------------------- /internal/presenters/unserve.go: -------------------------------------------------------------------------------- 1 | package presenters 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/eleven-sh/cli/internal/features" 7 | "github.com/eleven-sh/cli/internal/views" 8 | ) 9 | 10 | type UnserveViewer interface { 11 | View(views.UnserveViewData) 12 | } 13 | 14 | type UnservePresenter struct { 15 | viewableErrorBuilder ViewableErrorBuilder 16 | viewer UnserveViewer 17 | } 18 | 19 | func NewUnservePresenter( 20 | viewableErrorBuilder ViewableErrorBuilder, 21 | viewer UnserveViewer, 22 | ) UnservePresenter { 23 | 24 | return UnservePresenter{ 25 | viewableErrorBuilder: viewableErrorBuilder, 26 | viewer: viewer, 27 | } 28 | } 29 | 30 | func (u UnservePresenter) PresentToView(response features.UnserveResponse) { 31 | viewData := views.UnserveViewData{} 32 | 33 | if response.Error == nil { 34 | unservedPort := response.Content.Port 35 | 36 | viewDataMessage := fmt.Sprintf( 37 | "The port \"%s\" is now unreachable from outside.", 38 | unservedPort, 39 | ) 40 | 41 | viewData.Content = views.UnserveViewDataContent{ 42 | Message: viewDataMessage, 43 | } 44 | 45 | u.viewer.View(viewData) 46 | return 47 | } 48 | 49 | viewData.Error = u.viewableErrorBuilder.Build(response.Error) 50 | u.viewer.View(viewData) 51 | } 52 | -------------------------------------------------------------------------------- /internal/ssh/config.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/eleven-sh/cli/internal/system" 9 | "github.com/kevinburke/ssh_config" 10 | ) 11 | 12 | const ConfigFilePerm os.FileMode = 0644 13 | 14 | type Config struct { 15 | configFilePath string 16 | } 17 | 18 | func NewConfig(configFilePath string) Config { 19 | return Config{ 20 | configFilePath: configFilePath, 21 | } 22 | } 23 | 24 | func NewConfigWithDefaultConfigFilePath() Config { 25 | return NewConfig(system.DefaultSSHConfigFilePath()) 26 | } 27 | 28 | func (c Config) AddOrReplaceHost( 29 | hostKey string, 30 | hostName string, 31 | identityFile string, 32 | user string, 33 | port int64, 34 | ) error { 35 | 36 | cfg, err := c.parse() 37 | 38 | if err != nil { 39 | return err 40 | } 41 | 42 | hostPattern, err := ssh_config.NewPattern(hostKey) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | 48 | hostNodes := []ssh_config.Node{ 49 | &ssh_config.Empty{ 50 | Comment: " added by Eleven", 51 | }, 52 | 53 | &ssh_config.KV{ 54 | Key: " HostName", 55 | Value: hostName, 56 | }, 57 | 58 | &ssh_config.KV{ 59 | Key: " IdentityFile", 60 | Value: identityFile, 61 | }, 62 | 63 | &ssh_config.KV{ 64 | Key: " User", 65 | Value: user, 66 | }, 67 | 68 | &ssh_config.KV{ 69 | Key: " Port", 70 | Value: fmt.Sprint(port), 71 | }, 72 | 73 | &ssh_config.KV{ 74 | Key: " ForwardAgent", 75 | Value: "yes", 76 | }, 77 | } 78 | 79 | hostToAdd := &ssh_config.Host{ 80 | Patterns: []*ssh_config.Pattern{ 81 | hostPattern, 82 | }, 83 | Nodes: hostNodes, 84 | } 85 | 86 | hostToAddIndex := c.lookupHostIndex( 87 | cfg, 88 | hostKey, 89 | ) 90 | 91 | if hostToAddIndex == -1 { 92 | cfg.Hosts = append(cfg.Hosts, hostToAdd) 93 | } else { 94 | cfg.Hosts[hostToAddIndex] = hostToAdd 95 | } 96 | 97 | return c.save(cfg) 98 | } 99 | 100 | func (c Config) UpdateHost( 101 | hostKey string, 102 | hostName *string, 103 | identityFile *string, 104 | user *string, 105 | ) error { 106 | 107 | cfg, err := c.parse() 108 | 109 | if err != nil { 110 | return err 111 | } 112 | 113 | updatedHosts := []*ssh_config.Host{} 114 | 115 | for _, host := range cfg.Hosts { 116 | // We don't use "host.Matches()" 117 | // here because we don't want the 118 | // wildcard host ("Host *") to match 119 | if len(host.Patterns) == 1 && host.Patterns[0].String() == hostKey { 120 | for _, node := range host.Nodes { 121 | switch t := node.(type) { 122 | case *ssh_config.KV: 123 | lowercasedKey := strings.ToLower(t.Key) 124 | 125 | if lowercasedKey == "hostname" && hostName != nil { 126 | t.Value = *hostName 127 | } 128 | 129 | if lowercasedKey == "identityfile" && identityFile != nil { 130 | t.Value = *identityFile 131 | } 132 | 133 | if lowercasedKey == "user" && user != nil { 134 | t.Value = *user 135 | } 136 | } 137 | } 138 | } 139 | 140 | updatedHosts = append(updatedHosts, host) 141 | } 142 | 143 | cfg.Hosts = updatedHosts 144 | 145 | return c.save(cfg) 146 | } 147 | 148 | func (c Config) RemoveHostIfExists(hostKey string) error { 149 | cfg, err := c.parse() 150 | 151 | if err != nil { 152 | return err 153 | } 154 | 155 | updatedHosts := []*ssh_config.Host{} 156 | 157 | for _, host := range cfg.Hosts { 158 | // We don't use "host.Matches()" 159 | // here because we don't want the 160 | // wildcard host ("Host *") to match 161 | if len(host.Patterns) == 1 && host.Patterns[0].String() == hostKey { 162 | continue 163 | } 164 | 165 | updatedHosts = append(updatedHosts, host) 166 | } 167 | 168 | cfg.Hosts = updatedHosts 169 | 170 | return c.save(cfg) 171 | } 172 | 173 | func (c Config) CountEntriesWithHostPrefix(hostPrefix string) (int, error) { 174 | cfg, err := c.parse() 175 | 176 | if err != nil { 177 | return 0, err 178 | } 179 | 180 | count := 0 181 | 182 | for _, host := range cfg.Hosts { 183 | if len(host.Patterns) == 1 && 184 | strings.HasPrefix(host.Patterns[0].String(), hostPrefix) { 185 | 186 | count++ 187 | } 188 | } 189 | 190 | return count, nil 191 | } 192 | 193 | func (c Config) lookupHostIndex( 194 | cfg *ssh_config.Config, 195 | hostKey string, 196 | ) int { 197 | 198 | for hostIndex, host := range cfg.Hosts { 199 | // We don't use "host.Matches()" 200 | // here because we don't want the 201 | // wildcard host ("Host *") to match 202 | if len(host.Patterns) == 1 && host.Patterns[0].String() == hostKey { 203 | return hostIndex 204 | } 205 | 206 | continue 207 | } 208 | 209 | return -1 210 | } 211 | 212 | func (c Config) parse() (*ssh_config.Config, error) { 213 | f, err := os.OpenFile( 214 | c.configFilePath, 215 | os.O_CREATE|os.O_RDONLY, 216 | ConfigFilePerm, 217 | ) 218 | 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | defer f.Close() 224 | 225 | return ssh_config.Decode(f) 226 | } 227 | 228 | func (c Config) save(cfg *ssh_config.Config) error { 229 | return os.WriteFile( 230 | c.configFilePath, 231 | []byte(cfg.String()), 232 | ConfigFilePerm, 233 | ) 234 | } 235 | -------------------------------------------------------------------------------- /internal/ssh/config_remove_host_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/eleven-sh/cli/internal/ssh" 9 | "github.com/eleven-sh/cli/internal/system" 10 | ) 11 | 12 | func TestConfigRemoveHostWithExistingHost(t *testing.T) { 13 | configPath := "./testdata/non_empty_ssh_config" 14 | configAtStart, err := os.ReadFile(configPath) 15 | 16 | if err != nil { 17 | t.Fatalf("expected no error, got '%+v'", err) 18 | } 19 | 20 | defer func() { // Reset modified config file 21 | err = os.WriteFile( 22 | configPath, 23 | configAtStart, 24 | ssh.ConfigFilePerm, 25 | ) 26 | 27 | if err != nil { 28 | t.Fatalf("expected no error, got '%+v'", err) 29 | } 30 | }() 31 | 32 | expectedConfig := `Host * 33 | AddKeysToAgent yes 34 | UseKeychain yes 35 | IdentityFile ~/.ssh/id_rsa 36 | IdentitiesOnly yes 37 | ServerAliveInterval 240 38 | ` 39 | 40 | config := ssh.NewConfig(configPath) 41 | err = config.RemoveHostIfExists("34.128.204.12") 42 | 43 | if err != nil { 44 | t.Fatalf("expected no error, got '%+v'", err) 45 | } 46 | 47 | configAtEnd, err := os.ReadFile(configPath) 48 | 49 | if err != nil { 50 | t.Fatalf("expected no error, got '%+v'", err) 51 | } 52 | 53 | configAtEndString := strings.TrimSuffix( 54 | string(configAtEnd), 55 | system.NewLineChar, 56 | ) 57 | 58 | if configAtEndString != expectedConfig { 59 | t.Fatalf( 60 | "expected config to equal '%s', got '%s'", 61 | expectedConfig, 62 | configAtEndString, 63 | ) 64 | } 65 | } 66 | 67 | func TestConfigRemoveHostWithNonExistingHost(t *testing.T) { 68 | configPath := "./testdata/non_empty_ssh_config" 69 | configAtStart, err := os.ReadFile(configPath) 70 | 71 | if err != nil { 72 | t.Fatalf("expected no error, got '%+v'", err) 73 | } 74 | 75 | defer func() { // Reset modified config file 76 | err = os.WriteFile( 77 | configPath, 78 | configAtStart, 79 | ssh.ConfigFilePerm, 80 | ) 81 | 82 | if err != nil { 83 | t.Fatalf("expected no error, got '%+v'", err) 84 | } 85 | }() 86 | 87 | expectedConfig := string(configAtStart) 88 | 89 | config := ssh.NewConfig(configPath) 90 | err = config.RemoveHostIfExists("34.228.204.12") 91 | 92 | if err != nil { 93 | t.Fatalf("expected no error, got '%+v'", err) 94 | } 95 | 96 | configAtEnd, err := os.ReadFile(configPath) 97 | 98 | if err != nil { 99 | t.Fatalf("expected no error, got '%+v'", err) 100 | } 101 | 102 | configAtEndString := strings.TrimSuffix( 103 | string(configAtEnd), 104 | system.NewLineChar, 105 | ) 106 | 107 | if configAtEndString != expectedConfig { 108 | t.Fatalf( 109 | "expected config to equal '%s', got '%s'", 110 | expectedConfig, 111 | configAtEndString, 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/ssh/config_update_host_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/eleven-sh/cli/internal/ssh" 9 | "github.com/eleven-sh/cli/internal/system" 10 | ) 11 | 12 | func TestConfigUpdateHostWithExistingHost(t *testing.T) { 13 | configPath := "./testdata/non_empty_ssh_config" 14 | configAtStart, err := os.ReadFile(configPath) 15 | 16 | if err != nil { 17 | t.Fatalf("expected no error, got '%+v'", err) 18 | } 19 | 20 | defer func() { // Reset modified config file 21 | err = os.WriteFile( 22 | configPath, 23 | configAtStart, 24 | ssh.ConfigFilePerm, 25 | ) 26 | 27 | if err != nil { 28 | t.Fatalf("expected no error, got '%+v'", err) 29 | } 30 | }() 31 | 32 | expectedConfig := `Host * 33 | AddKeysToAgent yes 34 | UseKeychain yes 35 | IdentityFile ~/.ssh/id_rsa 36 | IdentitiesOnly yes 37 | ServerAliveInterval 240 38 | 39 | Host 34.128.204.12 40 | HostName updated_hostname 41 | IdentityFile updated_identityFile 42 | User updated_user 43 | ForwardAgent yes` 44 | 45 | config := ssh.NewConfig(configPath) 46 | 47 | updatedHostName := "updated_hostname" 48 | updatedUser := "updated_user" 49 | updatedIdentityFile := "updated_identityFile" 50 | 51 | err = config.UpdateHost( 52 | "34.128.204.12", 53 | &updatedHostName, 54 | &updatedIdentityFile, 55 | &updatedUser, 56 | ) 57 | 58 | if err != nil { 59 | t.Fatalf("expected no error, got '%+v'", err) 60 | } 61 | 62 | configAtEnd, err := os.ReadFile(configPath) 63 | 64 | if err != nil { 65 | t.Fatalf("expected no error, got '%+v'", err) 66 | } 67 | 68 | configAtEndString := strings.TrimSuffix( 69 | string(configAtEnd), 70 | system.NewLineChar, 71 | ) 72 | 73 | if configAtEndString != expectedConfig { 74 | t.Fatalf( 75 | "expected config to equal '%s', got '%s'", 76 | expectedConfig, 77 | configAtEndString, 78 | ) 79 | } 80 | } 81 | 82 | func TestConfigUpdateHostWithExistingHostAndPartialConfig(t *testing.T) { 83 | configPath := "./testdata/non_empty_ssh_config" 84 | configAtStart, err := os.ReadFile(configPath) 85 | 86 | if err != nil { 87 | t.Fatalf("expected no error, got '%+v'", err) 88 | } 89 | 90 | defer func() { // Reset modified config file 91 | err = os.WriteFile( 92 | configPath, 93 | configAtStart, 94 | ssh.ConfigFilePerm, 95 | ) 96 | 97 | if err != nil { 98 | t.Fatalf("expected no error, got '%+v'", err) 99 | } 100 | }() 101 | 102 | expectedConfig := `Host * 103 | AddKeysToAgent yes 104 | UseKeychain yes 105 | IdentityFile ~/.ssh/id_rsa 106 | IdentitiesOnly yes 107 | ServerAliveInterval 240 108 | 109 | Host 34.128.204.12 110 | HostName updated_hostname 111 | IdentityFile identityFile 112 | User user 113 | ForwardAgent yes` 114 | 115 | config := ssh.NewConfig(configPath) 116 | 117 | updatedHostName := "updated_hostname" 118 | 119 | err = config.UpdateHost( 120 | "34.128.204.12", 121 | &updatedHostName, 122 | nil, 123 | nil, 124 | ) 125 | 126 | if err != nil { 127 | t.Fatalf("expected no error, got '%+v'", err) 128 | } 129 | 130 | configAtEnd, err := os.ReadFile(configPath) 131 | 132 | if err != nil { 133 | t.Fatalf("expected no error, got '%+v'", err) 134 | } 135 | 136 | configAtEndString := strings.TrimSuffix( 137 | string(configAtEnd), 138 | system.NewLineChar, 139 | ) 140 | 141 | if configAtEndString != expectedConfig { 142 | t.Fatalf( 143 | "expected config to equal '%s', got '%s'", 144 | expectedConfig, 145 | configAtEndString, 146 | ) 147 | } 148 | } 149 | 150 | func TestConfigUpdateHostWithNonExistingHost(t *testing.T) { 151 | configPath := "./testdata/non_empty_ssh_config" 152 | configAtStart, err := os.ReadFile(configPath) 153 | 154 | if err != nil { 155 | t.Fatalf("expected no error, got '%+v'", err) 156 | } 157 | 158 | defer func() { // Reset modified config file 159 | err = os.WriteFile( 160 | configPath, 161 | configAtStart, 162 | ssh.ConfigFilePerm, 163 | ) 164 | 165 | if err != nil { 166 | t.Fatalf("expected no error, got '%+v'", err) 167 | } 168 | }() 169 | 170 | expectedConfig := string(configAtStart) 171 | 172 | config := ssh.NewConfig(configPath) 173 | 174 | updatedHostName := "updated_hostname" 175 | updatedUser := "updated_user" 176 | updatedIdentityFile := "updated_identityFile" 177 | 178 | err = config.UpdateHost( 179 | "34.228.204.12", 180 | &updatedHostName, 181 | &updatedIdentityFile, 182 | &updatedUser, 183 | ) 184 | 185 | if err != nil { 186 | t.Fatalf("expected no error, got '%+v'", err) 187 | } 188 | 189 | configAtEnd, err := os.ReadFile(configPath) 190 | 191 | if err != nil { 192 | t.Fatalf("expected no error, got '%+v'", err) 193 | } 194 | 195 | configAtEndString := strings.TrimSuffix( 196 | string(configAtEnd), 197 | system.NewLineChar, 198 | ) 199 | 200 | if configAtEndString != expectedConfig { 201 | t.Fatalf( 202 | "expected config to equal '%s', got '%s'", 203 | expectedConfig, 204 | configAtEndString, 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /internal/ssh/keys.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/eleven-sh/cli/internal/system" 10 | ) 11 | 12 | const PrivateKeyFilePerm os.FileMode = 0600 13 | 14 | type Keys struct { 15 | sshDir string 16 | } 17 | 18 | func NewKeys(SSHDir string) Keys { 19 | return Keys{ 20 | sshDir: SSHDir, 21 | } 22 | } 23 | 24 | func NewKeysWithDefaultDir() Keys { 25 | return NewKeys( 26 | system.DefaultSSHDir(), 27 | ) 28 | } 29 | 30 | func (k Keys) CreateOrReplacePEM( 31 | PEMName string, 32 | PEMContent string, 33 | ) (pathWritten string, err error) { 34 | 35 | pathWritten = filepath.Join(k.sshDir, PEMName+".pem") 36 | 37 | err = os.WriteFile( 38 | pathWritten, 39 | []byte(PEMContent), 40 | PrivateKeyFilePerm, 41 | ) 42 | 43 | return 44 | } 45 | 46 | func (k Keys) RemovePEMIfExists(PEMName string) error { 47 | err := os.Remove( 48 | k.GetPEMFilePath(PEMName), 49 | ) 50 | 51 | if err != nil && errors.Is(err, fs.ErrNotExist) { 52 | return nil 53 | } 54 | 55 | return err 56 | } 57 | 58 | func (k Keys) GetPEMFilePath(PEMName string) string { 59 | return filepath.Join( 60 | k.sshDir, 61 | PEMName+".pem", 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /internal/ssh/keys_add_pem_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/eleven-sh/cli/internal/ssh" 8 | ) 9 | 10 | func TestKeysCreateOrReplacePEM(t *testing.T) { 11 | keys := ssh.NewKeys("./testdata") 12 | 13 | expectedPEMName := "pem_name" 14 | expectedPEMContent := "pem_content" 15 | expectedPEMPath := "./testdata/" + expectedPEMName + ".pem" 16 | 17 | PEMPath, err := keys.CreateOrReplacePEM( 18 | expectedPEMName, 19 | expectedPEMContent, 20 | ) 21 | 22 | if err != nil { 23 | t.Fatalf("expected no error, got '%+v'", err) 24 | } 25 | 26 | defer func() { // Remove created PEM file 27 | err = os.Remove(PEMPath) 28 | 29 | if err != nil { 30 | t.Fatalf("expected no error, got '%+v'", err) 31 | } 32 | }() 33 | 34 | if "./"+PEMPath != expectedPEMPath { 35 | t.Fatalf( 36 | "expected PEM path to equal '%s', got '%s'", 37 | expectedPEMPath, 38 | PEMPath, 39 | ) 40 | } 41 | 42 | createdPEMContent, err := os.ReadFile(expectedPEMPath) 43 | 44 | if err != nil { 45 | t.Fatalf("expected no error, got '%+v'", err) 46 | } 47 | 48 | if string(createdPEMContent) != expectedPEMContent { 49 | t.Fatalf( 50 | "expected PEM to equal '%s', got '%s'", 51 | expectedPEMContent, 52 | string(createdPEMContent), 53 | ) 54 | } 55 | 56 | createdPEMFileInfo, err := os.Stat(expectedPEMPath) 57 | 58 | if err != nil { 59 | t.Fatalf("expected no error, got '%+v'", err) 60 | } 61 | 62 | if createdPEMFileInfo.Mode().Perm() != ssh.PrivateKeyFilePerm { 63 | t.Fatalf( 64 | "expected created PEM file to have permission '%o', got '%o'", 65 | ssh.PrivateKeyFilePerm, 66 | createdPEMFileInfo.Mode().Perm(), 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/ssh/keys_remove_pem_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | "testing" 8 | 9 | "github.com/eleven-sh/cli/internal/ssh" 10 | ) 11 | 12 | func TestKeysRemoveExistingPEM(t *testing.T) { 13 | keys := ssh.NewKeys("./testdata") 14 | 15 | PEMName := "pem_to_remove" 16 | PEMPath := "./testdata/" + PEMName + ".pem" 17 | 18 | _, err := os.Create(PEMPath) 19 | 20 | if err != nil { 21 | t.Fatalf("expected no error, got '%+v'", err) 22 | } 23 | 24 | err = keys.RemovePEMIfExists(PEMName) 25 | 26 | if err != nil { 27 | t.Fatalf("expected no error, got '%+v'", err) 28 | } 29 | 30 | _, err = os.Stat(PEMPath) 31 | 32 | if err == nil { 33 | t.Fatalf("expected file not exists error, got nothing") 34 | } 35 | 36 | if !errors.Is(err, fs.ErrNotExist) { 37 | t.Fatalf("expected file not exists error, got '%+v'", err) 38 | } 39 | } 40 | 41 | func TestKeysRemoveNonExistingPEM(t *testing.T) { 42 | keys := ssh.NewKeys("./testdata") 43 | err := keys.RemovePEMIfExists("non_existing_pem") 44 | 45 | if err != nil { 46 | t.Fatalf("expected no error, got '%+v'", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/ssh/known_hosts.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | 8 | "github.com/eleven-sh/cli/internal/system" 9 | ) 10 | 11 | const KnownHostsFilePerm os.FileMode = 0644 12 | 13 | type KnownHosts struct { 14 | knownHostsFilePath string 15 | } 16 | 17 | func NewKnownHosts(knownHostsFilePath string) KnownHosts { 18 | return KnownHosts{ 19 | knownHostsFilePath: knownHostsFilePath, 20 | } 21 | } 22 | 23 | func NewKnownHostsWithDefaultKnownHostsFilePath() KnownHosts { 24 | return NewKnownHosts( 25 | system.DefaultSSHKnownHostsFilePath(), 26 | ) 27 | } 28 | 29 | func (k KnownHosts) AddOrReplace(hostname, algorithm, fingerprint string) error { 30 | f, err := k.openFile() 31 | 32 | if err != nil { 33 | return err 34 | } 35 | 36 | defer f.Close() 37 | 38 | knownHostToAdd := hostname + " " + algorithm + " " + fingerprint 39 | knownHostToAddReplaced := false 40 | 41 | scanner := bufio.NewScanner(f) 42 | newKnownHostsContent := "" 43 | 44 | for scanner.Scan() { 45 | knownHostLine := scanner.Text() 46 | 47 | if strings.HasPrefix(knownHostLine, hostname+" "+algorithm) { 48 | newKnownHostsContent += knownHostToAdd + system.NewLineChar 49 | knownHostToAddReplaced = true 50 | continue 51 | } 52 | 53 | newKnownHostsContent += knownHostLine + system.NewLineChar 54 | } 55 | 56 | if err := scanner.Err(); err != nil { 57 | return err 58 | } 59 | 60 | if !knownHostToAddReplaced { 61 | newKnownHostsContent += knownHostToAdd + system.NewLineChar 62 | } 63 | 64 | return os.WriteFile( 65 | k.knownHostsFilePath, 66 | []byte(newKnownHostsContent), 67 | KnownHostsFilePerm, 68 | ) 69 | } 70 | 71 | func (k KnownHosts) RemoveIfExists(hostname string) error { 72 | if len(hostname) == 0 { 73 | // Nothing to do. 74 | // We don't want to remove all hostnames 75 | // (the function "hasPrefix" will always return "true" if prefix is empty). 76 | // See below. 77 | return nil 78 | } 79 | 80 | f, err := k.openFile() 81 | 82 | if err != nil { 83 | return err 84 | } 85 | 86 | defer f.Close() 87 | 88 | scanner := bufio.NewScanner(f) 89 | newKnownHostsContent := "" 90 | 91 | for scanner.Scan() { 92 | knownHostLine := scanner.Text() 93 | 94 | if strings.HasPrefix(knownHostLine, hostname) { 95 | continue 96 | } 97 | 98 | newKnownHostsContent += knownHostLine + system.NewLineChar 99 | } 100 | 101 | if err := scanner.Err(); err != nil { 102 | return err 103 | } 104 | 105 | /* We only want one new line 106 | at the end of the file */ 107 | 108 | for { 109 | trimedNewKnownHostsContent := strings.TrimSuffix( 110 | newKnownHostsContent, 111 | system.NewLineChar, 112 | ) 113 | 114 | if trimedNewKnownHostsContent == newKnownHostsContent { 115 | break 116 | } 117 | 118 | newKnownHostsContent = trimedNewKnownHostsContent 119 | } 120 | 121 | newKnownHostsContent += system.NewLineChar 122 | 123 | return os.WriteFile( 124 | k.knownHostsFilePath, 125 | []byte(newKnownHostsContent), 126 | KnownHostsFilePerm, 127 | ) 128 | } 129 | 130 | func (k KnownHosts) openFile() (*os.File, error) { 131 | // create the "known_hosts" file if necessary 132 | return os.OpenFile( 133 | k.knownHostsFilePath, 134 | os.O_APPEND|os.O_CREATE|os.O_RDWR, 135 | KnownHostsFilePerm, 136 | ) 137 | } 138 | -------------------------------------------------------------------------------- /internal/ssh/known_hosts_remove_test.go: -------------------------------------------------------------------------------- 1 | package ssh_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/eleven-sh/cli/internal/ssh" 8 | ) 9 | 10 | func TestKnownHostsRemoveWithExistingHostname(t *testing.T) { 11 | knownHostsPath := "./testdata/non_empty_known_hosts" 12 | knownHostsAtStart, err := os.ReadFile(knownHostsPath) 13 | 14 | if err != nil { 15 | t.Fatalf("expected no error, got '%+v'", err) 16 | } 17 | 18 | defer func() { // Reset modified known hosts file 19 | err = os.WriteFile( 20 | knownHostsPath, 21 | knownHostsAtStart, 22 | ssh.KnownHostsFilePerm, 23 | ) 24 | 25 | if err != nil { 26 | t.Fatalf("expected no error, got '%+v'", err) 27 | } 28 | }() 29 | 30 | expectedKnownHosts := `github.com,140.82.118.3 ssh-rsa fingerprint 31 | 32 | github.com ecdsa-sha2-nistp256 fingerprint 33 | github.com ssh-ed25519 fingerprint 34 | ` 35 | 36 | knownHosts := ssh.NewKnownHosts(knownHostsPath) 37 | err = knownHosts.RemoveIfExists("34.229.126.51") 38 | 39 | if err != nil { 40 | t.Fatalf("expected no error, got '%+v'", err) 41 | } 42 | 43 | knownHostsAtEnd, err := os.ReadFile(knownHostsPath) 44 | 45 | if err != nil { 46 | t.Fatalf("expected no error, got '%+v'", err) 47 | } 48 | 49 | if string(knownHostsAtEnd) != expectedKnownHosts { 50 | t.Fatalf( 51 | "expected known hosts to equal '%s', got '%s'", 52 | expectedKnownHosts, 53 | string(knownHostsAtEnd), 54 | ) 55 | } 56 | } 57 | 58 | func TestKnownHostsRemoveWithNonExistingHostname(t *testing.T) { 59 | knownHostsPath := "./testdata/non_empty_known_hosts" 60 | knownHostsAtStart, err := os.ReadFile(knownHostsPath) 61 | 62 | if err != nil { 63 | t.Fatalf("expected no error, got '%+v'", err) 64 | } 65 | 66 | defer func() { // Reset modified known hosts file 67 | err = os.WriteFile( 68 | knownHostsPath, 69 | knownHostsAtStart, 70 | ssh.KnownHostsFilePerm, 71 | ) 72 | 73 | if err != nil { 74 | t.Fatalf("expected no error, got '%+v'", err) 75 | } 76 | }() 77 | 78 | expectedKnownHosts := knownHostsAtStart 79 | 80 | knownHosts := ssh.NewKnownHosts(knownHostsPath) 81 | err = knownHosts.RemoveIfExists("104.78.1.4") 82 | 83 | if err != nil { 84 | t.Fatalf("expected no error, got '%+v'", err) 85 | } 86 | 87 | knownHostsAtEnd, err := os.ReadFile(knownHostsPath) 88 | 89 | if err != nil { 90 | t.Fatalf("expected no error, got '%+v'", err) 91 | } 92 | 93 | if string(expectedKnownHosts) != string(knownHostsAtEnd) { 94 | t.Fatalf( 95 | "expected known hosts to equal '%s', got '%s'", 96 | string(expectedKnownHosts), 97 | string(knownHostsAtEnd), 98 | ) 99 | } 100 | } 101 | 102 | func TestKnownHostsRemoveWithEmptyHostname(t *testing.T) { 103 | knownHostsPath := "./testdata/non_empty_known_hosts" 104 | knownHostsAtStart, err := os.ReadFile(knownHostsPath) 105 | 106 | if err != nil { 107 | t.Fatalf("expected no error, got '%+v'", err) 108 | } 109 | 110 | defer func() { // Reset modified known hosts file 111 | err = os.WriteFile( 112 | knownHostsPath, 113 | knownHostsAtStart, 114 | ssh.KnownHostsFilePerm, 115 | ) 116 | 117 | if err != nil { 118 | t.Fatalf("expected no error, got '%+v'", err) 119 | } 120 | }() 121 | 122 | expectedKnownHosts := knownHostsAtStart 123 | 124 | knownHosts := ssh.NewKnownHosts(knownHostsPath) 125 | err = knownHosts.RemoveIfExists("") 126 | 127 | if err != nil { 128 | t.Fatalf("expected no error, got '%+v'", err) 129 | } 130 | 131 | knownHostsAtEnd, err := os.ReadFile(knownHostsPath) 132 | 133 | if err != nil { 134 | t.Fatalf("expected no error, got '%+v'", err) 135 | } 136 | 137 | if string(expectedKnownHosts) != string(knownHostsAtEnd) { 138 | t.Fatalf( 139 | "expected known hosts to equal '%s', got '%s'", 140 | string(expectedKnownHosts), 141 | string(knownHostsAtEnd), 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /internal/ssh/port_forwarding.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | type PortForwarder struct{} 12 | 13 | func NewPortForwarder() PortForwarder { 14 | return PortForwarder{} 15 | } 16 | 17 | type PortForwarderReadyResp struct { 18 | Error error 19 | LocalAddr string 20 | } 21 | 22 | func (p PortForwarder) Forward( 23 | onReadyChan chan<- PortForwarderReadyResp, 24 | privateKeyBytes []byte, 25 | user string, 26 | serverAddr string, 27 | localAddr string, 28 | remoteAddrProtocol string, 29 | remoteAddr string, 30 | ) error { 31 | 32 | sshConfig, err := p.buildSSHConfig( 33 | 8*time.Second, 34 | user, 35 | privateKeyBytes, 36 | ) 37 | 38 | if err != nil { 39 | onReadyChan <- PortForwarderReadyResp{ 40 | Error: err, 41 | } 42 | return nil 43 | } 44 | 45 | // Establish connection with server through SSH 46 | serverSSHConn, err := ssh.Dial("tcp", serverAddr, sshConfig) 47 | 48 | if err != nil { 49 | onReadyChan <- PortForwarderReadyResp{ 50 | Error: err, 51 | } 52 | return nil 53 | } 54 | 55 | defer serverSSHConn.Close() 56 | 57 | // Establish connection with remoteAddr from server 58 | remoteConn, err := serverSSHConn.Dial(remoteAddrProtocol, remoteAddr) 59 | 60 | if err != nil { 61 | onReadyChan <- PortForwarderReadyResp{ 62 | Error: err, 63 | } 64 | return nil 65 | } 66 | 67 | // Start local TCP server to forward traffic to remote connection 68 | localTCPServer, err := net.Listen("tcp", localAddr) 69 | 70 | if err != nil { 71 | onReadyChan <- PortForwarderReadyResp{ 72 | Error: err, 73 | } 74 | return nil 75 | } 76 | 77 | defer localTCPServer.Close() 78 | 79 | onReadyChan <- PortForwarderReadyResp{ 80 | LocalAddr: localTCPServer.Addr().String(), 81 | } 82 | 83 | localConn, err := localTCPServer.Accept() 84 | 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return p.forwardLocalToRemoteConn( 90 | localConn, 91 | remoteConn, 92 | ) 93 | } 94 | 95 | // Get ssh client config for our connection 96 | func (p PortForwarder) buildSSHConfig( 97 | connTimeout time.Duration, 98 | user string, 99 | privateKeyBytes []byte, 100 | ) (*ssh.ClientConfig, error) { 101 | 102 | parsedPrivateKey, err := ssh.ParsePrivateKey(privateKeyBytes) 103 | 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | config := ssh.ClientConfig{ 109 | User: user, 110 | Auth: []ssh.AuthMethod{ 111 | ssh.PublicKeys(parsedPrivateKey), 112 | }, 113 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 114 | Timeout: connTimeout, 115 | } 116 | 117 | return &config, nil 118 | } 119 | 120 | // Handle local TCP server connections and tunnel data to the remote server 121 | func (p PortForwarder) forwardLocalToRemoteConn( 122 | localConn net.Conn, 123 | remoteConn net.Conn, 124 | ) error { 125 | 126 | defer func() { 127 | localConn.Close() 128 | remoteConn.Close() 129 | }() 130 | 131 | remoteConnRespChan := make(chan error, 1) 132 | localConnRespChan := make(chan error, 1) 133 | 134 | // Forward remote -> local 135 | go func() { 136 | _, err := io.Copy(localConn, remoteConn) 137 | remoteConnRespChan <- err 138 | }() 139 | 140 | // Forward local -> remote 141 | go func() { 142 | _, err := io.Copy(remoteConn, localConn) 143 | localConnRespChan <- err 144 | }() 145 | 146 | select { 147 | case remoteConnErr := <-remoteConnRespChan: 148 | return remoteConnErr 149 | case localConnErr := <-localConnRespChan: 150 | return localConnErr 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/ssh/testdata/empty_known_hosts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-sh/cli/47872ad01d9dd7032da356c2d91b256a231f0d1b/internal/ssh/testdata/empty_known_hosts -------------------------------------------------------------------------------- /internal/ssh/testdata/empty_ssh_config: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eleven-sh/cli/47872ad01d9dd7032da356c2d91b256a231f0d1b/internal/ssh/testdata/empty_ssh_config -------------------------------------------------------------------------------- /internal/ssh/testdata/non_empty_known_hosts: -------------------------------------------------------------------------------- 1 | github.com,140.82.118.3 ssh-rsa fingerprint 2 | 34.229.126.51 ssh-rsa fingerprint 3 | 4 | github.com ecdsa-sha2-nistp256 fingerprint 5 | github.com ssh-ed25519 fingerprint 6 | 7 | 34.229.126.51 ecdsa-sha2-nistp256 fingerprint 8 | 34.229.126.51 ssh-ed25519 fingerprint 9 | -------------------------------------------------------------------------------- /internal/ssh/testdata/non_empty_ssh_config: -------------------------------------------------------------------------------- 1 | Host * 2 | AddKeysToAgent yes 3 | UseKeychain yes 4 | IdentityFile ~/.ssh/id_rsa 5 | IdentitiesOnly yes 6 | ServerAliveInterval 240 7 | 8 | Host 34.128.204.12 9 | HostName hostname 10 | IdentityFile identityFile 11 | User user 12 | ForwardAgent yes -------------------------------------------------------------------------------- /internal/stepper/step.go: -------------------------------------------------------------------------------- 1 | package stepper 2 | 3 | import ( 4 | "github.com/briandowns/spinner" 5 | "github.com/eleven-sh/cli/internal/interfaces" 6 | ) 7 | 8 | type Step struct { 9 | logger interfaces.Logger 10 | spin *spinner.Spinner 11 | removeAfterDone bool 12 | } 13 | 14 | func (s *Step) Done() { 15 | s.spin.Stop() 16 | 17 | if !s.removeAfterDone { 18 | s.logger.Log(s.spin.Prefix + "... done") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/stepper/stepper.go: -------------------------------------------------------------------------------- 1 | package stepper 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/briandowns/spinner" 7 | "github.com/eleven-sh/cli/internal/config" 8 | "github.com/eleven-sh/cli/internal/interfaces" 9 | "github.com/eleven-sh/eleven/stepper" 10 | ) 11 | 12 | var currentStep *Step 13 | 14 | type Stepper struct { 15 | logger interfaces.Logger 16 | } 17 | 18 | func NewStepper( 19 | logger interfaces.Logger, 20 | ) Stepper { 21 | 22 | return Stepper{ 23 | logger: logger, 24 | } 25 | } 26 | 27 | func (s Stepper) startStep( 28 | step string, 29 | removeAfterDone bool, 30 | noNewLineAtStart bool, 31 | ) stepper.Step { 32 | 33 | if currentStep == nil && !noNewLineAtStart { 34 | s.logger.Log("") 35 | } 36 | 37 | if currentStep != nil { 38 | currentStep.Done() 39 | currentStep = nil 40 | } 41 | 42 | bold := config.ColorsBold 43 | 44 | spin := spinner.New(spinner.CharSets[26], 400*time.Millisecond) 45 | spin.Prefix = bold(step) 46 | spin.Start() 47 | 48 | currentStep = &Step{ 49 | logger: s.logger, 50 | spin: spin, 51 | removeAfterDone: removeAfterDone, 52 | } 53 | 54 | return currentStep 55 | } 56 | 57 | func (s Stepper) StartStep( 58 | step string, 59 | ) stepper.Step { 60 | 61 | removeAfterDone := false 62 | noNewLineAtStart := false 63 | 64 | return s.startStep( 65 | step, 66 | removeAfterDone, 67 | noNewLineAtStart, 68 | ) 69 | } 70 | 71 | func (s Stepper) StartTemporaryStep( 72 | step string, 73 | ) stepper.Step { 74 | 75 | removeAfterDone := true 76 | noNewLineAtStart := false 77 | 78 | return s.startStep( 79 | step, 80 | removeAfterDone, 81 | noNewLineAtStart, 82 | ) 83 | } 84 | 85 | func (s Stepper) StartTemporaryStepWithoutNewLine( 86 | step string, 87 | ) stepper.Step { 88 | 89 | removeAfterDone := true 90 | noNewLineAtStart := true 91 | 92 | return s.startStep( 93 | step, 94 | removeAfterDone, 95 | noNewLineAtStart, 96 | ) 97 | } 98 | 99 | func (s Stepper) StopCurrentStep() { 100 | 101 | if currentStep != nil { 102 | currentStep.Done() 103 | currentStep = nil 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/system/browser.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/pkg/browser" 5 | ) 6 | 7 | type Browser struct{} 8 | 9 | func NewBrowser() Browser { 10 | return Browser{} 11 | } 12 | 13 | func (Browser) OpenURL(url string) error { 14 | return browser.OpenURL(url) 15 | } 16 | -------------------------------------------------------------------------------- /internal/system/cli.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strings" 7 | 8 | "github.com/eleven-sh/cli/internal/config" 9 | "github.com/eleven-sh/cli/internal/interfaces" 10 | ) 11 | 12 | func AskForConfirmation( 13 | logger interfaces.Logger, 14 | stdin io.Reader, 15 | question string, 16 | ) (bool, error) { 17 | 18 | stdinReader := bufio.NewReader(stdin) 19 | 20 | logger.Log(config.ColorsBold(config.ColorsYellow("Warning!") + " " + question)) 21 | 22 | logger.Log("\nOnly \"yes\" will be accepted to confirm. (You could use \"--force\" next time).\n") 23 | logger.LogNoNewline(config.ColorsBold("Confirm? ")) 24 | 25 | response, err := stdinReader.ReadString('\n') 26 | 27 | if err != nil { 28 | return false, err 29 | } 30 | 31 | sanitizedResponse := strings.TrimSpace(response) 32 | 33 | if sanitizedResponse == "yes" { 34 | return true, nil 35 | } 36 | 37 | logger.Log("") 38 | 39 | return false, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/system/displayer.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type Displayer struct{} 9 | 10 | func NewDisplayer() Displayer { 11 | return Displayer{} 12 | } 13 | 14 | func (Displayer) Display(w io.Writer, format string, args ...interface{}) { 15 | toDisplay := replaceNewLinesForOS( 16 | fmt.Sprintf(format, args...), 17 | ) 18 | 19 | fmt.Fprint(w, toDisplay) 20 | } 21 | -------------------------------------------------------------------------------- /internal/system/env_vars.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "os" 4 | 5 | type EnvVars struct{} 6 | 7 | func NewEnvVars() EnvVars { 8 | return EnvVars{} 9 | } 10 | 11 | func (EnvVars) Get(key string) string { 12 | return os.Getenv(key) 13 | } 14 | -------------------------------------------------------------------------------- /internal/system/logger.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/eleven-sh/cli/internal/config" 8 | ) 9 | 10 | type Logger struct{} 11 | 12 | func NewLogger() Logger { 13 | return Logger{} 14 | } 15 | 16 | func (Logger) Info(format string, v ...interface{}) { 17 | toDisplay := replaceNewLinesForOS( 18 | fmt.Sprintf(format+"\n", v...), 19 | ) 20 | 21 | fmt.Fprint(os.Stderr, config.ColorsCyan(toDisplay)) 22 | } 23 | 24 | func (Logger) Warning(format string, v ...interface{}) { 25 | toDisplay := replaceNewLinesForOS( 26 | fmt.Sprintf(format+"\n", v...), 27 | ) 28 | 29 | fmt.Fprint(os.Stderr, config.ColorsYellow(toDisplay)) 30 | } 31 | 32 | func (Logger) Error(format string, v ...interface{}) { 33 | toDisplay := replaceNewLinesForOS( 34 | fmt.Sprintf(format+"\n", v...), 35 | ) 36 | 37 | fmt.Fprint(os.Stderr, config.ColorsRed(toDisplay)) 38 | } 39 | 40 | func (Logger) Log(format string, v ...interface{}) { 41 | toDisplay := replaceNewLinesForOS( 42 | fmt.Sprintf(format+"\n", v...), 43 | ) 44 | 45 | fmt.Fprint(os.Stderr, toDisplay) 46 | } 47 | 48 | func (Logger) LogNoNewline(format string, v ...interface{}) { 49 | toDisplay := replaceNewLinesForOS( 50 | fmt.Sprintf(format, v...), 51 | ) 52 | 53 | fmt.Fprint(os.Stderr, toDisplay) 54 | } 55 | 56 | func (l Logger) Write(p []byte) (n int, err error) { 57 | l.Log(string(p)) 58 | return len(p), nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/system/new_line.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | ) 7 | 8 | var NewLineChar = "\n" 9 | 10 | func init() { 11 | if runtime.GOOS == "windows" { 12 | NewLineChar = "\r\n" 13 | } 14 | } 15 | 16 | func replaceNewLinesForOS(s string) string { 17 | return strings.ReplaceAll(s, "\n", NewLineChar) 18 | } 19 | -------------------------------------------------------------------------------- /internal/system/paths.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | func PathExists(path string) bool { 9 | _, err := os.Stat(path) 10 | 11 | if err == nil || !os.IsNotExist(err) { 12 | return true 13 | } 14 | 15 | return false 16 | } 17 | 18 | func UserHomeDir() string { 19 | // Ignore errors since we only care about Windows and *nix. 20 | homedir, _ := os.UserHomeDir() 21 | return homedir 22 | } 23 | 24 | // UserConfigDir returns the path where 25 | // the user config files should be stored 26 | // following XDG Base Directory Specification. 27 | // Ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html 28 | func UserConfigDir() string { 29 | baseConfigDir := os.Getenv("XDG_CONFIG_HOME") 30 | 31 | if len(baseConfigDir) == 0 { 32 | baseConfigDir = filepath.Join(UserHomeDir(), ".config") 33 | } 34 | 35 | return filepath.Join(baseConfigDir, "eleven") 36 | } 37 | 38 | func UserConfigFilePath() string { 39 | return filepath.Join(UserConfigDir(), "eleven.yml") 40 | } 41 | 42 | func DefaultSSHDir() string { 43 | return filepath.Join(UserHomeDir(), ".ssh") 44 | } 45 | 46 | func DefaultSSHDirExists() bool { 47 | return PathExists(DefaultSSHDir()) 48 | } 49 | 50 | func DefaultSSHConfigFilePath() string { 51 | return filepath.Join(DefaultSSHDir(), "config") 52 | } 53 | 54 | func DefaultSSHConfigFileExists() bool { 55 | return PathExists(DefaultSSHConfigFilePath()) 56 | } 57 | 58 | func DefaultSSHKnownHostsFilePath() string { 59 | return filepath.Join(DefaultSSHDir(), "known_hosts") 60 | } 61 | 62 | func DefaultSSHKnownHostsFileExists() bool { 63 | return PathExists(DefaultSSHKnownHostsFilePath()) 64 | } 65 | -------------------------------------------------------------------------------- /internal/system/paths_test.go: -------------------------------------------------------------------------------- 1 | package system_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/eleven-sh/cli/internal/system" 9 | ) 10 | 11 | func TestPathExistsWithExistingPath(t *testing.T) { 12 | existingPath := "./paths_test.go" 13 | pathExists := system.PathExists(existingPath) 14 | 15 | if !pathExists { 16 | t.Fatalf("expected 'true', got 'false'") 17 | } 18 | } 19 | 20 | func TestPathExistsWithNonExistingPath(t *testing.T) { 21 | nonExistingPath := "./path-that-doesnt-exist" 22 | pathExists := system.PathExists(nonExistingPath) 23 | 24 | if pathExists { 25 | t.Fatalf("expected 'false', got 'true'") 26 | } 27 | } 28 | 29 | func TestUserHomeDir(t *testing.T) { 30 | expectedHomeDir, err := os.UserHomeDir() 31 | 32 | if err != nil { 33 | t.Fatalf("expected no error, got '%+v'", err) 34 | } 35 | 36 | if system.UserHomeDir() != expectedHomeDir { 37 | t.Fatalf( 38 | "expected user home directory to equal '%s', got '%s'", 39 | expectedHomeDir, 40 | system.UserHomeDir(), 41 | ) 42 | } 43 | } 44 | 45 | func TestDefaultSSHDir(t *testing.T) { 46 | homeDir, err := os.UserHomeDir() 47 | 48 | if err != nil { 49 | t.Fatalf("expected no error, got '%+v'", err) 50 | } 51 | 52 | expectedSSHDir := filepath.Join(homeDir, ".ssh") 53 | 54 | if system.DefaultSSHDir() != expectedSSHDir { 55 | t.Fatalf( 56 | "expected default SSH directory to equal '%s', got '%s'", 57 | expectedSSHDir, 58 | system.DefaultSSHDir(), 59 | ) 60 | } 61 | } 62 | 63 | func TestDefaultSSHConfigFilePath(t *testing.T) { 64 | homeDir, err := os.UserHomeDir() 65 | 66 | if err != nil { 67 | t.Fatalf("expected no error, got '%+v'", err) 68 | } 69 | 70 | expectedSSHConfigFilePath := filepath.Join(homeDir, ".ssh/config") 71 | 72 | if system.DefaultSSHConfigFilePath() != expectedSSHConfigFilePath { 73 | t.Fatalf( 74 | "expected default SSH config file path to equal '%s', got '%s'", 75 | expectedSSHConfigFilePath, 76 | system.DefaultSSHConfigFilePath(), 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/system/sleeper.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Sleeper struct{} 8 | 9 | func NewSleeper() Sleeper { 10 | return Sleeper{} 11 | } 12 | 13 | func (Sleeper) Sleep(d time.Duration) { 14 | time.Sleep(d) 15 | } 16 | -------------------------------------------------------------------------------- /internal/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // following https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 5 | 6 | package internal 7 | 8 | import ( 9 | _ "github.com/golang/mock/mockgen" 10 | _ "github.com/google/wire/cmd/wire" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/views/base.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | 8 | "github.com/eleven-sh/cli/internal/config" 9 | ) 10 | 11 | //go:generate go run github.com/golang/mock/mockgen -destination=../mocks/views_displayer.go -package=mocks github.com/eleven-sh/cli/internal/views Displayer 12 | type Displayer interface { 13 | Display(w io.Writer, format string, args ...interface{}) 14 | } 15 | 16 | type BaseView struct { 17 | Displayer Displayer 18 | } 19 | 20 | func NewBaseView(displayer Displayer) BaseView { 21 | return BaseView{ 22 | Displayer: displayer, 23 | } 24 | } 25 | 26 | func (b BaseView) showErrorView( 27 | err *ViewableError, 28 | startWithNewLine bool, 29 | ) { 30 | 31 | bold := config.ColorsBold 32 | red := config.ColorsRed 33 | 34 | if startWithNewLine { 35 | b.Displayer.Display( 36 | os.Stdout, 37 | "\n", 38 | ) 39 | } 40 | 41 | if len(err.Logs) > 0 { 42 | b.Displayer.Display( 43 | os.Stdout, 44 | "%s\n\n", 45 | strings.TrimSuffix(err.Logs, "\n"), 46 | ) 47 | } 48 | 49 | b.Displayer.Display( 50 | os.Stdout, 51 | "%s %s\n\n%s\n\n", 52 | bold(red("Error!")), 53 | bold(err.Title), 54 | err.Message, 55 | ) 56 | } 57 | 58 | func (b BaseView) ShowErrorView(err *ViewableError) { 59 | b.showErrorView(err, false) 60 | } 61 | 62 | func (b BaseView) ShowErrorViewWithStartingNewLine(err *ViewableError) { 63 | b.showErrorView(err, true) 64 | } 65 | 66 | func (b BaseView) ShowWarningView(warningText, subtext string) { 67 | bold := config.ColorsBold 68 | yellow := config.ColorsYellow 69 | 70 | if len(subtext) > 0 { 71 | b.Displayer.Display( 72 | os.Stdout, 73 | "%s %s\n\n%s\n\n", 74 | bold(yellow("Warning!")), 75 | bold(warningText), 76 | subtext, 77 | ) 78 | 79 | return 80 | } 81 | 82 | b.Displayer.Display( 83 | os.Stdout, 84 | "%s %s\n\n", 85 | bold(yellow("Warning!")), 86 | bold(warningText), 87 | ) 88 | } 89 | 90 | func (b BaseView) ShowSuccessView(successText, subtext string) { 91 | bold := config.ColorsBold 92 | green := config.ColorsGreen 93 | 94 | if len(subtext) > 0 { 95 | b.Displayer.Display( 96 | os.Stdout, 97 | "%s %s\n\n%s\n\n", 98 | bold(green("Success!")), 99 | bold(successText), 100 | subtext, 101 | ) 102 | 103 | return 104 | } 105 | 106 | b.Displayer.Display( 107 | os.Stdout, 108 | "%s %s\n\n", 109 | bold(green("Success!")), 110 | bold(successText), 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /internal/views/edit.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type EditViewDataContent struct { 4 | Message string 5 | } 6 | 7 | type EditViewData struct { 8 | Error *ViewableError 9 | Content EditViewDataContent 10 | } 11 | 12 | type EditView struct { 13 | BaseView 14 | } 15 | 16 | func NewEditView(baseView BaseView) EditView { 17 | return EditView{ 18 | BaseView: baseView, 19 | } 20 | } 21 | 22 | func (e EditView) View(data EditViewData) { 23 | if data.Error == nil { 24 | e.ShowSuccessView(data.Content.Message, "") 25 | return 26 | } 27 | 28 | e.ShowErrorView(data.Error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/views/errors.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type ViewableError struct { 4 | Title string 5 | Message string 6 | Logs string 7 | } 8 | -------------------------------------------------------------------------------- /internal/views/init.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type InitViewData struct { 4 | Error *ViewableError 5 | Content InitViewDataContent 6 | } 7 | 8 | type InitViewDataContent struct { 9 | ShowAsWarning bool 10 | Message string 11 | Subtext string 12 | } 13 | 14 | type InitView struct { 15 | BaseView 16 | } 17 | 18 | func NewInitView(baseView BaseView) InitView { 19 | return InitView{ 20 | BaseView: baseView, 21 | } 22 | } 23 | 24 | func (i InitView) View(data InitViewData) { 25 | if data.Error == nil { 26 | if data.Content.ShowAsWarning { 27 | i.ShowWarningView( 28 | data.Content.Message, 29 | data.Content.Subtext, 30 | ) 31 | return 32 | } 33 | 34 | i.ShowSuccessView( 35 | data.Content.Message, 36 | data.Content.Subtext, 37 | ) 38 | return 39 | } 40 | 41 | i.ShowErrorView(data.Error) 42 | } 43 | -------------------------------------------------------------------------------- /internal/views/login.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type LoginViewDataContent struct { 4 | Message string 5 | } 6 | 7 | type LoginViewData struct { 8 | Error *ViewableError 9 | Content LoginViewDataContent 10 | } 11 | 12 | type LoginView struct { 13 | BaseView 14 | } 15 | 16 | func NewLoginView(baseView BaseView) LoginView { 17 | return LoginView{ 18 | BaseView: baseView, 19 | } 20 | } 21 | 22 | func (l LoginView) View(data LoginViewData) { 23 | if data.Error == nil { 24 | l.ShowSuccessView(data.Content.Message, "") 25 | return 26 | } 27 | 28 | l.ShowErrorView(data.Error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/views/remove.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type RemoveViewDataContent struct { 4 | Message string 5 | } 6 | 7 | type RemoveViewData struct { 8 | Error *ViewableError 9 | Content RemoveViewDataContent 10 | } 11 | 12 | type RemoveView struct { 13 | BaseView 14 | } 15 | 16 | func NewRemoveView(baseView BaseView) RemoveView { 17 | return RemoveView{ 18 | BaseView: baseView, 19 | } 20 | } 21 | 22 | func (r RemoveView) View(data RemoveViewData) { 23 | if data.Error == nil { 24 | r.ShowSuccessView(data.Content.Message, "") 25 | return 26 | } 27 | 28 | r.ShowErrorView(data.Error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/views/serve.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type ServeViewDataContent struct { 4 | Message string 5 | } 6 | 7 | type ServeViewData struct { 8 | Error *ViewableError 9 | Content ServeViewDataContent 10 | } 11 | 12 | type ServeView struct { 13 | BaseView 14 | } 15 | 16 | func NewServeView(baseView BaseView) ServeView { 17 | return ServeView{ 18 | BaseView: baseView, 19 | } 20 | } 21 | 22 | func (s ServeView) View(data ServeViewData) { 23 | if data.Error == nil { 24 | s.ShowSuccessView(data.Content.Message, "") 25 | return 26 | } 27 | 28 | s.ShowErrorView(data.Error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/views/uninstall.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type UninstallViewDataContent struct { 4 | ShowAsWarning bool 5 | Message string 6 | Subtext string 7 | } 8 | 9 | type UninstallViewData struct { 10 | Error *ViewableError 11 | Content UninstallViewDataContent 12 | } 13 | 14 | type UninstallView struct { 15 | BaseView 16 | } 17 | 18 | func NewUninstallView(baseView BaseView) UninstallView { 19 | return UninstallView{ 20 | BaseView: baseView, 21 | } 22 | } 23 | 24 | func (u UninstallView) View(data UninstallViewData) { 25 | if data.Error == nil { 26 | if data.Content.ShowAsWarning { 27 | u.ShowWarningView( 28 | data.Content.Message, 29 | data.Content.Subtext, 30 | ) 31 | return 32 | } 33 | 34 | u.ShowSuccessView( 35 | data.Content.Message, 36 | data.Content.Subtext, 37 | ) 38 | return 39 | } 40 | 41 | u.ShowErrorView(data.Error) 42 | } 43 | -------------------------------------------------------------------------------- /internal/views/unserve.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | type UnserveViewDataContent struct { 4 | Message string 5 | } 6 | 7 | type UnserveViewData struct { 8 | Error *ViewableError 9 | Content UnserveViewDataContent 10 | } 11 | 12 | type UnserveView struct { 13 | BaseView 14 | } 15 | 16 | func NewUnserveView(baseView BaseView) UnserveView { 17 | return UnserveView{ 18 | BaseView: baseView, 19 | } 20 | } 21 | 22 | func (u UnserveView) View(data UnserveViewData) { 23 | if data.Error == nil { 24 | u.ShowSuccessView(data.Content.Message, "") 25 | return 26 | } 27 | 28 | u.ShowErrorView(data.Error) 29 | } 30 | -------------------------------------------------------------------------------- /internal/vscode/cli.go: -------------------------------------------------------------------------------- 1 | package vscode 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | 11 | "github.com/eleven-sh/cli/internal/exceptions" 12 | "github.com/eleven-sh/cli/internal/system" 13 | ) 14 | 15 | type ErrCLINotFound struct { 16 | VisitedPaths []string 17 | } 18 | 19 | func (ErrCLINotFound) Error() string { 20 | return "ErrCLINotFound" 21 | } 22 | 23 | type CLI struct{} 24 | 25 | func (c CLI) Exec(arg ...string) (string, error) { 26 | CLIPath, err := c.LookupPath(runtime.GOOS) 27 | 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | cmd := exec.Command(CLIPath, arg...) 33 | 34 | var stdBuf bytes.Buffer 35 | 36 | cmd.Stdout = &stdBuf 37 | cmd.Stderr = &stdBuf 38 | 39 | err = cmd.Run() 40 | 41 | if err != nil { 42 | return "", exceptions.ErrVSCodeError{ 43 | Logs: stdBuf.String(), 44 | ErrorMessage: err.Error(), 45 | } 46 | } 47 | 48 | return stdBuf.String(), nil 49 | } 50 | 51 | func (c CLI) LookupPath(operatingSystem string) (string, error) { 52 | // First, we look for the 'code-insiders' command 53 | insidersCLIPath, err := exec.LookPath("code-insiders") 54 | 55 | if err == nil { // 'code-insiders' command exists 56 | return insidersCLIPath, nil 57 | } 58 | 59 | // If the 'code-insiders' command was not found, we look for the 'code' one 60 | CLIPath, err := exec.LookPath("code") 61 | 62 | if err == nil { // 'code' command exists 63 | return CLIPath, nil 64 | } 65 | 66 | // Finally, we fallback to default paths 67 | possibleCLIPaths := []string{} 68 | 69 | if operatingSystem == "darwin" { // macOS 70 | possibleCLIPaths = c.macOSPossibleCLIPaths() 71 | } 72 | 73 | if operatingSystem == "windows" { 74 | possibleCLIPaths = c.windowsPossibleCLIPaths() 75 | } 76 | 77 | if operatingSystem == "linux" { 78 | possibleCLIPaths = c.linuxPossibleCLIPaths() 79 | } 80 | 81 | for _, possibleCLIPath := range possibleCLIPaths { 82 | if system.PathExists(possibleCLIPath) { 83 | return possibleCLIPath, nil 84 | } 85 | } 86 | 87 | return "", ErrCLINotFound{ 88 | VisitedPaths: possibleCLIPaths, 89 | } 90 | } 91 | 92 | func (c CLI) macOSPossibleCLIPaths() []string { 93 | rootApplicationsDir := fmt.Sprintf("%cApplications", os.PathSeparator) // /Applications 94 | 95 | // Order matter here. 96 | // We want the insiders version to be matched first. 97 | possiblePaths := []string{} 98 | 99 | possiblePaths = append(possiblePaths, filepath.Join( 100 | rootApplicationsDir, 101 | "Visual Studio Code - Insiders.app", 102 | "Contents", 103 | "Resources", 104 | "app", 105 | "bin", 106 | "code-insiders", 107 | )) 108 | 109 | possiblePaths = append(possiblePaths, filepath.Join( 110 | rootApplicationsDir, 111 | "Visual Studio Code.app", 112 | "Contents", 113 | "Resources", 114 | "app", 115 | "bin", 116 | "code", 117 | )) 118 | 119 | return possiblePaths 120 | } 121 | 122 | func (c CLI) windowsPossibleCLIPaths() []string { 123 | programFilesPath := os.Getenv("ProgramFiles") 124 | 125 | // Order matter here. 126 | // We want the insiders version to be matched first. 127 | 128 | // -- Insiders VSCode versions 129 | 130 | possiblePaths := []string{} 131 | 132 | possiblePaths = append(possiblePaths, filepath.Join( 133 | system.UserHomeDir(), 134 | "AppData", 135 | "Local", 136 | "Programs", 137 | "Microsoft VS Code Insiders", 138 | "bin", 139 | "code-insiders.cmd", 140 | )) 141 | 142 | possiblePaths = append(possiblePaths, filepath.Join( 143 | programFilesPath, 144 | "Microsoft VS Code Insiders", 145 | "bin", 146 | "code-insiders.cmd", 147 | )) 148 | 149 | // -- Regular VSCode versions 150 | 151 | possiblePaths = append(possiblePaths, filepath.Join( 152 | system.UserHomeDir(), 153 | "AppData", 154 | "Local", 155 | "Programs", 156 | "Microsoft VS Code", 157 | "bin", 158 | "code.cmd", 159 | )) 160 | 161 | possiblePaths = append(possiblePaths, filepath.Join( 162 | programFilesPath, 163 | "Microsoft VS Code", 164 | "bin", 165 | "code.cmd", 166 | )) 167 | 168 | return possiblePaths 169 | } 170 | 171 | func (c CLI) linuxPossibleCLIPaths() []string { 172 | // Order matter here. 173 | // We want the insiders version to be matched first. 174 | possiblePaths := []string{ 175 | "/usr/bin/code-insiders", 176 | "/snap/bin/code-insiders", 177 | "/usr/share/code/bin/code-insiders", 178 | 179 | "/usr/bin/code", 180 | "/snap/bin/code", 181 | "/usr/share/code/bin/code", 182 | } 183 | 184 | return possiblePaths 185 | } 186 | -------------------------------------------------------------------------------- /internal/vscode/extensions.go: -------------------------------------------------------------------------------- 1 | package vscode 2 | 3 | type Extensions struct{} 4 | 5 | func NewExtensions() Extensions { 6 | return Extensions{} 7 | } 8 | 9 | func (e Extensions) Install(extensionName string) (string, error) { 10 | c := CLI{} 11 | 12 | return c.Exec( 13 | "--install-extension", 14 | extensionName, 15 | "--force", 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /internal/vscode/process.go: -------------------------------------------------------------------------------- 1 | package vscode 2 | 3 | type Process struct{} 4 | 5 | func NewProcess() Process { 6 | return Process{} 7 | } 8 | 9 | func (p Process) OpenOnRemote(hostKey, pathToOpen string) (string, error) { 10 | c := CLI{} 11 | 12 | return c.Exec( 13 | "--new-window", 14 | "--skip-release-notes", 15 | "--skip-welcome", 16 | "--skip-add-to-recently-opened", 17 | "--disable-workspace-trust", 18 | "--remote", 19 | "ssh-remote+"+hostKey, 20 | pathToOpen, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 Jeremy Levy jje.levy@gmail.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | package main 23 | 24 | import "github.com/eleven-sh/cli/internal/cmd" 25 | 26 | func main() { 27 | cmd.Execute() 28 | } 29 | --------------------------------------------------------------------------------