Full Code of superbrothers/opener for AI

master 95903db0f9b8 cached
22 files
20.6 KB
7.1k tokens
10 symbols
1 requests
Download .txt
Repository: superbrothers/opener
Branch: master
Commit: 95903db0f9b8
Files: 22
Total size: 20.6 KB

Directory structure:
gitextract_ih7yev_u/

├── .dockerignore
├── .github/
│   ├── renovate.json
│   └── workflows/
│       ├── ci.yaml
│       └── release.yaml
├── .gitignore
├── .goreleaser.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── open
│   └── xdg-open
├── config.go
├── config_test.go
├── go.mod
├── go.sum
├── main.go
├── opener.go
├── opener_test.go
└── testdata/
    └── config/
        ├── empty.yaml
        ├── tcp.yaml
        └── unix.yaml

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
/dist


================================================
FILE: .github/renovate.json
================================================
{
  "extends": ["config:base"],
  "labels": ["renovate"],
  "enabledManagers": ["dockerfile", "regex", "github-actions"],
  "regexManagers": [
    {
      "fileMatch": ["(^|/)Makefile$"],
      "matchStrings": [
        "#\\s*renovate:\\s*datasource=(?<datasource>.*?)\\s+depName=(?<depName>.*?)(\\s+versioning=(?<versioning>.*?))?(\\s+registry=(?<registryUrl>.*?))?\\s.*?_VERSION\\s+[^=]?=\\s+(?<currentValue>.*)\\s"
      ],
      "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}"
    }
  ]
}


================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI

on:
  push:
    branches: [master]
    paths-ignore: ['**.md']
  pull_request:
    types: [opened, synchronize]
    paths-ignore: ['**.md']

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version-file: go.mod
    - name: Ensure go.mod is already tidied
      run: go mod tidy && git diff -s --exit-code go.sum
    - run: make lint
    - run: make test
    - run: make build


================================================
FILE: .github/workflows/release.yaml
================================================
name: Release

on:
  push:
    tags: ["v*"]

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-go@v5
      with:
        go-version-file: go.mod
    - run: make release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
/dist
/hack/tools/bin


================================================
FILE: .goreleaser.yaml
================================================
version: 2
before:
  hooks:
  - go mod tidy
project_name: opener
builds:
- env:
  - CGO_ENABLED=0
  goos:
  - linux
  - darwin
  goarch:
  - amd64
  - arm
  - arm64
archives:
- name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
  formats:
  - zip
  files:
  - LICENSE
  - README.md
  wrap_in_directory: false
checksum:
  name_template: 'checksums.txt'


================================================
FILE: Dockerfile
================================================
FROM --platform=${BUILDPLATFORM} golang:1.24 AS base
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

FROM base AS build
ARG TARGETOS
ARG TARGETARCH
RUN --mount=target=. \
    --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/opener .

FROM base AS test
RUN --mount=target=. \
    --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go test -v ./...

FROM golangci/golangci-lint:v1.64.8 AS lint-base
FROM base AS lint
RUN --mount=target=. \
    --mount=from=lint-base,src=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
    --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/root/.cache/golangci-lint \
    go vet ./... && \
    go fmt ./... && \
    golangci-lint run

FROM scratch AS bin-unix
COPY --from=build /out/opener /

FROM bin-unix AS bin-linux
FROM bin-unix AS bin-darwin

FROM bin-${TARGETOS} as bin


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Kazuki Suda <kazuki.suda@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
NAME := opener
DIST_DIR := dist
GO ?= go
VERSION ?= $(shell git describe --tags --always --dirty)

PLATFORM ?= local
DOCKER ?= DOCKER_BUILDKIT=1 docker

.PHONY: build
build:
	$(DOCKER) build --target bin --output $(DIST_DIR) --platform $(PLATFORM) .

TOOLS_BIN_DIR := $(CURDIR)/hack/tools/bin
$(shell mkdir -p $(TOOLS_BIN_DIR))

# renovate: datasource=github-releases depName=goreleaser/goreleaser
GORELEASER_VERSION ?= v2.8.1
GORELEASER := $(TOOLS_BIN_DIR)/goreleaser

$(GORELEASER):
	GOBIN=$(TOOLS_BIN_DIR) $(GO) install github.com/goreleaser/goreleaser/v2@$(GORELEASER_VERSION)

.PHONY: build-cross
build-cross: $(GORELEASER)
	$(GORELEASER) build --snapshot --clean

.PHONY: test
test:
	$(DOCKER) build --target test .

.PHONY: lint
lint:
	$(DOCKER) build --target lint .

.PHONY: dist
dist: $(GORELEASER)
	$(GORELEASER) release --clean --skip=publish --snapshot

.PHONY: release
release: $(GORELEASER)
	$(GORELEASER) release --clean --skip=validate

.PHONY: clean
clean: clean-tools clean-dist

.PHONY: clean-tools
clean-tools:
	rm -rf $(TOOLS_BIN_DIR)

.PHONY: clean-dist
clean-dist:
	rm -rf $(DIST_DIR)


================================================
FILE: README.md
================================================
# opener

![Logo](./sennuki.png)

Open URL in your local web browser from the SSH-connected remote environment.

## How does opener work?

opener is a daemon process that runs locally. When you send a URL to the process, it will execute a command tailored to your local environment (`open` on macOS, `xdg-open` on Linux) with the URL as an argument. As a result, the URL will be opened in your favorite web browser.

You remotely forward the socket file of the opener daemon, `~/.opener.sock`, when you log in to the remote environment via SSH. In a remote environment, you use fake `open` command or` xdg-open` command to send the URL to `~/.opener.sock` being forwarded from your local environment. The result is as if URL was sent to the local opener daemon, which opens the URL in your local web browser.

```
┌────────────────────┐                 ┌────────────────────┐
│                    │                 │                    │
│ ┌────────────────┐ │                 │ ┌────────────────┐ │
│ │   Web Browser  │ │                 │ │  open command  │ │
│ └─▲──────────────┘ │                 │ │     (fake)     │ │
│   │ Open URL       │                 │ └─┬──────────────┘ │
│ ┌─┴──────────────┐ │                 │   │                │
│ │  opener daemon │ │                 │   │ Send URL       │
│ └─┬──────────────┘ │                 │   │                │
│   │                │                 │   │                │
│ ┌─┴──────────────┐ │ SSH connection  │ ┌─▼──────────────┐ │
│ │ ~/.opener.sock │ ├─────────────────► │ ~/.opener.sock │ │
│ └────────────────┘ │ Remote forward  │ └────────────────┘ │
│                    │                 │                    │
│      localhost     │                 │    remote server   │
└────────────────────┘                 └────────────────────┘
```

## Setup

### Local environment

You can install opener with Homebrew. Since opener is a daemon, it is managed by Homebrew-services.

```
$ brew install superbrothers/opener/opener
$ brew services start opener
```

Set ssh config to forward `~/.opener.sock` to the remote environment.

```
Host host.example.org
  RemoteForward /home/me/.opener.sock /Users/me/.opener.sock
```

### Remote environment

Install a fake `open` or` xdg-open` command. Please choose your preference either way.

```sh
$ mkdir ~/bin
# open command
$ curl -L -o ~/bin/open https://raw.githubusercontent.com/superbrothers/opener/master/bin/open
$ chmod 755 ~/bin/open
# xdg-open command
$ curl -L -o ~/bin/xdg-open https://raw.githubusercontent.com/superbrothers/opener/master/bin/xdg-open
$ chmod 755 ~/bin/xdg-open
# Add ~/bin to $PATH and enable it
$ echo 'export PATH="$HOME/bin:$PATH"' >>~/.bashrc
$ source ~/.bashrc
```

Fake commands use `nc` command, so install it if you don't have it.

```sh
# Ubuntu 20.04
$ sudo apt install netcat
```

Add the following settings to sshd. This is an option to delete the socket file when you lose the connection to the remote environment.

```sh
# Add a configuration file
$ echo "StreamLocalBindUnlink yes" | sudo tee /etc/ssh/sshd_config.d/opener.conf
# Restart ssh service
$ sudo systemctl restart ssh
```

## How to use it

If set up correctly, the following command in a remote environment will send the URL through opener and open the URL in your local web browser.

```
$ open https://www.google.com/
```

## Configuration

You can configure opener with a config file. By default, it should be located at `~/.config/opener/config.yaml`. You can also specify a config file with `--config` option.

```yaml
# The network to use opener daemon.
# Allowed networks are: unix or tcp. (defaults to unix)
network: unix

# The address to listen on. (defaults to ~/.opener.sock)
address: ~/.opener.sock
```

### Example: Open a URL from inside a container

If you want to open a URL from inside a container, you can use `tcp` network instead of `unix`.

```
┌─────────────────────────────────────────────────────────┐
│                                                         │
│ ┌────────────────┐                                      │
│ │   Web Browser  │                                      │
│ └─▲──────────────┘                 ┌──────────────────┐ │
│   │                                │     container    │ │
│   │ Open URL                       │                  │ │
│   │                                │ ┌──────────────┐ │ │
│ ┌─┴──────────────┐    Send a URL   │ │ open command │ │ │
│ │  opener daemon │◄────────────────┼─┤    (fake)    │ │ │
│ └────────────────┘   (TCP request) │ └──────────────┘ │ │
│   127.0.0.1:9999                   │                  │ │
│                                    └──────────────────┘ │
│                       localhost                         │
└─────────────────────────────────────────────────────────┘
```

Create the following config at `~/.config/opener/config.yaml`:

```yaml
network: tcp
address: 127.0.0.1:9999
```

Restart the opener daemon:

```
$ brew services restart opener
```

Send a URL to the opener daemon from inside a container:

```
$ docker run --rm -it busybox /bin/sh
# echo https://www.google.com/ | nc host.docker.internal 9999
```

The following script is useful as a fake `open` command.

```sh
#!/bin/sh
echo "$@" | nc host.docker.internal 9999
```

## License

MIT License


================================================
FILE: bin/open
================================================
#!/bin/bash

set -eu

# get data either form stdin or from file
if (( $# == 0 )) ; then
  # if no argument, read from standard input from pipe
  buf=$(cat "$@")
else
  # otherwise read from all arguments
  buf=$@
fi

echo "$buf" | nc -U "$HOME/.opener.sock"


================================================
FILE: bin/xdg-open
================================================
#!/bin/bash

set -eu

# get data either form stdin or from file
if (( $# == 0 )) ; then
  # if no argument, read from standard input from pipe
  buf=$(cat "$@")
else
  # otherwise read from all arguments
  buf=$@
fi

echo "$buf" | nc -U "$HOME/.opener.sock"


================================================
FILE: config.go
================================================
package main

import (
	"os"
	"path/filepath"

	"sigs.k8s.io/yaml"
)

func LoadOpenerOptionsFromConfig(configPath string, o *OpenerOptions) error {
	if configPath == "" {
		dir, err := os.UserHomeDir()
		if err != nil {
			return err
		}

		configPath = filepath.Join(dir, ".config", "opener", "config.yaml")
		if _, err := os.Stat(configPath); err != nil {
			// The config file does not exist in the default path.
			return nil
		}
	} else {
		if _, err := os.Stat(configPath); err != nil {
			return err
		}
	}

	b, err := os.ReadFile(configPath)
	if err != nil {
		return err
	}

	if err := yaml.Unmarshal(b, o); err != nil {
		return err
	}

	return nil
}


================================================
FILE: config_test.go
================================================
package main

import (
	"path/filepath"
	"reflect"
	"testing"
)

func TestLoadOptionsFromConfig(t *testing.T) {
	tt := []struct {
		test        string
		configPath  string
		expected    *OpenerOptions
		expectedErr string
	}{
		{
			"unix",
			filepath.Join("testdata", "config", "unix.yaml"),
			&OpenerOptions{
				Network: "unix",
				Address: "~/.opener.sock",
			},
			"",
		},
		{
			"tcp",
			filepath.Join("testdata", "config", "tcp.yaml"),
			&OpenerOptions{
				Network: "tcp",
				Address: "127.0.0.1:9000",
			},
			"",
		},
		{
			"empty",
			filepath.Join("testdata", "config", "empty.yaml"),
			&OpenerOptions{},
			"",
		},
		{
			"no such file",
			filepath.Join("testdata", "config", "no-such-file.yaml"),
			&OpenerOptions{},
			"stat testdata/config/no-such-file.yaml: no such file or directory",
		},
	}

	for _, tc := range tt {
		t.Run(tc.test, func(t *testing.T) {
			o := &OpenerOptions{}
			err := LoadOpenerOptionsFromConfig(tc.configPath, o)
			if err == nil {
				if tc.expectedErr != "" {
					t.Errorf("expected err nil, but %q", err)
				}
			} else {
				if tc.expectedErr != err.Error() {
					t.Errorf("expected err %q, but %q", tc.expectedErr, err)
				}
			}

			if !reflect.DeepEqual(tc.expected, o) {
				t.Errorf("expected %#v, but %#v", tc.expected, o)
			}
		})
	}
}


================================================
FILE: go.mod
================================================
module github.com/superbrothers/opener

go 1.24.1

require (
	github.com/mitchellh/go-homedir v1.1.0
	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
	github.com/spf13/cobra v1.9.1
	sigs.k8s.io/yaml v1.4.0
)

require (
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/spf13/pflag v1.0.6 // indirect
	golang.org/x/sys v0.31.0 // indirect
)


================================================
FILE: go.sum
================================================
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=


================================================
FILE: main.go
================================================
package main

import (
	"log"
	"os"
)

func main() {
	cmd := NewOpenerCmd(os.Stderr)
	if err := cmd.Execute(); err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: opener.go
================================================
package main

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"

	"github.com/mitchellh/go-homedir"
	"github.com/pkg/browser"
	"github.com/spf13/cobra"
)

var version string
var commit string
var date string

type OpenerOptions struct {
	Network string `yaml:"network"`
	Address string `yaml:"address"`

	ErrOut io.Writer
}

func NewOpenerCmd(errOut io.Writer) *cobra.Command {
	var configPath string

	o := &OpenerOptions{
		Network: "unix",
		Address: "~/.opener.sock",
		ErrOut:  errOut,
	}

	cmd := &cobra.Command{
		Use: "opener",
		RunE: func(_ *cobra.Command, args []string) error {
			if err := LoadOpenerOptionsFromConfig(configPath, o); err != nil {
				return err
			}

			if err := o.Validate(); err != nil {
				return err
			}

			return o.Run()
		},
	}

	cmd.Flags().StringVar(&configPath, "config", configPath, "Path to the opener config file (defaults to ~/.config/opener/config.yaml)")

	return cmd
}

func (o *OpenerOptions) Validate() error {
	switch o.Network {
	case "unix":
		address, err := homedir.Expand(o.Address)
		if err != nil {
			return err
		}
		o.Address = address

		syscall.Umask(0077)

		if err := os.RemoveAll(o.Address); err != nil {
			return err
		}
	case "tcp":
	default:
		return errors.New("allowed network are: unix,tcp")
	}

	return nil
}

func (o *OpenerOptions) Run() error {
	fmt.Fprintf(o.ErrOut, "version: %s, commit: %s, date: %s\n", version, commit, date)
	fmt.Fprintf(o.ErrOut, "starting a server at %s\n", o.Address)

	ln, err := net.Listen(o.Network, o.Address)
	if err != nil {
		return err
	}

	defer ln.Close()

	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				fmt.Fprintln(o.ErrOut, err)
				return
			}

			go handleConnection(conn, o.ErrOut)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
	sig := <-c
	fmt.Fprintf(o.ErrOut, "got signal %s\n", sig)

	return nil
}

var browserMu sync.Mutex

var openURL = func(line string) (string, error) {
	// We try out best avoiding race-condition on swapping browser.{Stdout,Stderr}.
	// This works in a case when there are two or more consumers exist for this package.
	//
	// Fingers-crossed when github.com/pkg/browser is used concurrently outside of this package...
	browserMu.Lock()

	stdout, stderr := browser.Stdout, browser.Stderr

	defer func() {
		browser.Stdout = stdout
		browser.Stderr = stderr

		browserMu.Unlock()
	}()

	var buf bytes.Buffer

	browser.Stdout = &buf
	browser.Stderr = &buf

	err := browser.OpenURL(line)

	return buf.String(), err
}

func handleConnection(conn net.Conn, errOut io.Writer) {
	defer conn.Close()

	line, err := bufio.NewReader(conn).ReadString('\n')
	line = strings.TrimRight(line, "\n")
	fmt.Fprintf(errOut, "received %q\n", line)
	if err != nil {
		if err != io.EOF {
			fmt.Fprintln(errOut, err)
			return
		}
	}

	logs, err := openURL(line)

	if logs != "" {
		fmt.Fprint(errOut, logs)
	}

	if err != nil {
		fmt.Fprintf(errOut, "failed to open %q: %v\n", line, err)

		// Send back the logs from `open` to the client over e.g. the unix domain socket, so that
		// `open` on the client machine would work more like that on the server.
		//
		// Note that this works only when the client selected the protocol of SOCK_STREAM rather than e.g. SOCK_DGRAM.
		// `socat`, for example, negotiates the protocol to prefer SOCK_STREAM so you won't usually care.
		if _, err := conn.Write([]byte(logs)); err != nil {
			fmt.Fprintf(errOut, "failed to send error to client: %v\n", err)
		}
		return
	}
}


================================================
FILE: opener_test.go
================================================
package main

import (
	"errors"
	"fmt"
	"io"
	"math/rand"
	"net"
	"path/filepath"
	"testing"
)

func TestOpenerOptionsValidate(t *testing.T) {
	tt := []struct {
		test        string
		o           *OpenerOptions
		expectedErr string
	}{
		{
			"unix domain socket can be used",
			&OpenerOptions{
				Network: "unix",
				Address: filepath.Join("/", "tmp", fmt.Sprintf("%03d", rand.Intn(1000)), "opener.sock"),
			},
			"",
		},
		{
			"tcp can be used",
			&OpenerOptions{
				Network: "tcp",
				Address: "127.0.0.1:8888",
			},
			"",
		},
		{
			"udp cannot be used",
			&OpenerOptions{
				Network: "udp",
				Address: "127.0.0.1:8888",
			},
			"allowed network are: unix,tcp",
		},
	}

	for _, tc := range tt {
		t.Run(tc.test, func(t *testing.T) {
			err := tc.o.Validate()
			if err == nil {
				if tc.expectedErr != "" {
					t.Errorf("expect err nil, but actual %q", err)
				}
			} else {
				if tc.expectedErr != err.Error() {
					t.Errorf("expect err %q, but actual %q", tc.expectedErr, err)
				}
			}
		})
	}
}

func TestHandleConnection(t *testing.T) {
	tt := []struct {
		test        string
		openURLFunc func(string) (string, error)
		data        string
		err         error
	}{
		{
			"Say nothing when successful",
			func(line string) (string, error) {
				return "pong\n", nil
			},
			"",
			io.EOF,
		},
		{
			"Sending back the logs when failure",
			func(line string) (string, error) {
				return "pong\n", errors.New("exit status 1")
			},
			"pong\n",
			nil,
		},
	}

	ln, _ := net.Listen("tcp", "127.0.0.1:0")
	defer ln.Close()

	for _, tc := range tt {
		t.Run(tc.test, func(t *testing.T) {
			openURL = tc.openURLFunc

			go func() {
				conn, _ := ln.Accept()
				go handleConnection(conn, io.Discard)
			}()

			client, err := net.Dial("tcp", ln.Addr().String())
			if err != nil {
				t.Fatal(err)
			}
			defer client.Close()

			if _, err := client.Write([]byte("ping\n")); err != nil {
				t.Fatal(err)
			}

			buf := make([]byte, 1024)
			n, err := client.Read(buf)
			data := string(buf[:n])
			if tc.data != data {
				t.Errorf("expect %q, but actual %q", tc.data, data)
			}

			if tc.err != err {
				t.Errorf("expect %v, but actual %v", tc.err, err)
			}
		})
	}
}


================================================
FILE: testdata/config/empty.yaml
================================================


================================================
FILE: testdata/config/tcp.yaml
================================================
network: tcp
address: 127.0.0.1:9000


================================================
FILE: testdata/config/unix.yaml
================================================
network: unix
address: ~/.opener.sock
Download .txt
gitextract_ih7yev_u/

├── .dockerignore
├── .github/
│   ├── renovate.json
│   └── workflows/
│       ├── ci.yaml
│       └── release.yaml
├── .gitignore
├── .goreleaser.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bin/
│   ├── open
│   └── xdg-open
├── config.go
├── config_test.go
├── go.mod
├── go.sum
├── main.go
├── opener.go
├── opener_test.go
└── testdata/
    └── config/
        ├── empty.yaml
        ├── tcp.yaml
        └── unix.yaml
Download .txt
SYMBOL INDEX (10 symbols across 5 files)

FILE: config.go
  function LoadOpenerOptionsFromConfig (line 10) | func LoadOpenerOptionsFromConfig(configPath string, o *OpenerOptions) er...

FILE: config_test.go
  function TestLoadOptionsFromConfig (line 9) | func TestLoadOptionsFromConfig(t *testing.T) {

FILE: main.go
  function main (line 8) | func main() {

FILE: opener.go
  type OpenerOptions (line 25) | type OpenerOptions struct
    method Validate (line 61) | func (o *OpenerOptions) Validate() error {
    method Run (line 83) | func (o *OpenerOptions) Run() error {
  function NewOpenerCmd (line 32) | func NewOpenerCmd(errOut io.Writer) *cobra.Command {
  function handleConnection (line 142) | func handleConnection(conn net.Conn, errOut io.Writer) {

FILE: opener_test.go
  function TestOpenerOptionsValidate (line 13) | func TestOpenerOptionsValidate(t *testing.T) {
  function TestHandleConnection (line 61) | func TestHandleConnection(t *testing.T) {
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (25K chars).
[
  {
    "path": ".dockerignore",
    "chars": 6,
    "preview": "/dist\n"
  },
  {
    "path": ".github/renovate.json",
    "chars": 525,
    "preview": "{\n  \"extends\": [\"config:base\"],\n  \"labels\": [\"renovate\"],\n  \"enabledManagers\": [\"dockerfile\", \"regex\", \"github-actions\"]"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 475,
    "preview": "name: CI\n\non:\n  push:\n    branches: [master]\n    paths-ignore: ['**.md']\n  pull_request:\n    types: [opened, synchronize"
  },
  {
    "path": ".github/workflows/release.yaml",
    "chars": 289,
    "preview": "name: Release\n\non:\n  push:\n    tags: [\"v*\"]\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/chec"
  },
  {
    "path": ".gitignore",
    "chars": 22,
    "preview": "/dist\n/hack/tools/bin\n"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 359,
    "preview": "version: 2\nbefore:\n  hooks:\n  - go mod tidy\nproject_name: opener\nbuilds:\n- env:\n  - CGO_ENABLED=0\n  goos:\n  - linux\n  - "
  },
  {
    "path": "Dockerfile",
    "chars": 1101,
    "preview": "FROM --platform=${BUILDPLATFORM} golang:1.24 AS base\nWORKDIR /src\nENV CGO_ENABLED=0\nCOPY go.* .\nRUN --mount=type=cache,t"
  },
  {
    "path": "LICENSE",
    "chars": 1092,
    "preview": "MIT License\n\nCopyright (c) 2021 Kazuki Suda <kazuki.suda@gmail.com>\n\nPermission is hereby granted, free of charge, to an"
  },
  {
    "path": "Makefile",
    "chars": 1109,
    "preview": "NAME := opener\nDIST_DIR := dist\nGO ?= go\nVERSION ?= $(shell git describe --tags --always --dirty)\n\nPLATFORM ?= local\nDOC"
  },
  {
    "path": "README.md",
    "chars": 5284,
    "preview": "# opener\n\n![Logo](./sennuki.png)\n\nOpen URL in your local web browser from the SSH-connected remote environment.\n\n## How "
  },
  {
    "path": "bin/open",
    "chars": 258,
    "preview": "#!/bin/bash\n\nset -eu\n\n# get data either form stdin or from file\nif (( $# == 0 )) ; then\n  # if no argument, read from st"
  },
  {
    "path": "bin/xdg-open",
    "chars": 258,
    "preview": "#!/bin/bash\n\nset -eu\n\n# get data either form stdin or from file\nif (( $# == 0 )) ; then\n  # if no argument, read from st"
  },
  {
    "path": "config.go",
    "chars": 661,
    "preview": "package main\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"sigs.k8s.io/yaml\"\n)\n\nfunc LoadOpenerOptionsFromConfig(configPath string"
  },
  {
    "path": "config_test.go",
    "chars": 1309,
    "preview": "package main\n\nimport (\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestLoadOptionsFromConfig(t *testing.T) {\n\ttt := ["
  },
  {
    "path": "go.mod",
    "chars": 369,
    "preview": "module github.com/superbrothers/opener\n\ngo 1.24.1\n\nrequire (\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/pkg/bro"
  },
  {
    "path": "go.sum",
    "chars": 1947,
    "preview": "github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/google/go-cmp "
  },
  {
    "path": "main.go",
    "chars": 146,
    "preview": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n)\n\nfunc main() {\n\tcmd := NewOpenerCmd(os.Stderr)\n\tif err := cmd.Execute(); err != ni"
  },
  {
    "path": "opener.go",
    "chars": 3564,
    "preview": "package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n"
  },
  {
    "path": "opener_test.go",
    "chars": 2211,
    "preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestOpenerOptionsV"
  },
  {
    "path": "testdata/config/empty.yaml",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "testdata/config/tcp.yaml",
    "chars": 37,
    "preview": "network: tcp\naddress: 127.0.0.1:9000\n"
  },
  {
    "path": "testdata/config/unix.yaml",
    "chars": 38,
    "preview": "network: unix\naddress: ~/.opener.sock\n"
  }
]

About this extraction

This page contains the full source code of the superbrothers/opener GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (20.6 KB), approximately 7.1k tokens, and a symbol index with 10 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!