mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Add wizard cli
This commit is contained in:
@@ -10,6 +10,8 @@ server/auth-server
|
||||
server/claim-account
|
||||
server/octo-cdn
|
||||
server/dev
|
||||
server/wizard
|
||||
server/.wizard.json
|
||||
|
||||
__pycache__/
|
||||
|
||||
|
||||
@@ -15,6 +15,17 @@ Discord server: https://discord.gg/MZAf5aVkJG
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
```
|
||||
|
||||
### Quick Start (Wizard)
|
||||
|
||||
The interactive wizard walks you through setup with a few simple questions — no flags or networking knowledge needed. It auto-detects the right IP address for your emulator or phone and launches all services.
|
||||
|
||||
```bash
|
||||
cd server
|
||||
go run ./cmd/wizard
|
||||
```
|
||||
|
||||
Your choices are saved so next time you just press Enter to relaunch with the same settings.
|
||||
|
||||
### Regenerate protobuf stubs
|
||||
|
||||
```bash
|
||||
|
||||
+5
-2
@@ -3,13 +3,16 @@
|
||||
# (generating all would put them in one package and cause name clashes).
|
||||
PROTO_USED = proto/banner.proto proto/battle.proto proto/bighunt.proto proto/cageornament.proto proto/character.proto proto/characterboard.proto proto/characterviewer.proto proto/companion.proto proto/config.proto proto/consumableitem.proto proto/contentsstory.proto proto/costume.proto proto/data.proto proto/deck.proto proto/dokan.proto proto/explore.proto proto/friend.proto proto/gacha.proto proto/gameplay.proto proto/gift.proto proto/gimmick.proto proto/labyrinth.proto proto/loginbonus.proto proto/material.proto proto/mission.proto proto/movie.proto proto/navicutin.proto proto/omikuji.proto proto/notification.proto proto/parts.proto proto/portalcage.proto proto/pvp.proto proto/quest.proto proto/reward.proto proto/shop.proto proto/sidestoryquest.proto proto/tutorial.proto proto/user.proto proto/weapon.proto
|
||||
|
||||
PROTOC ?= protoc
|
||||
GOOSE ?= goose
|
||||
|
||||
EXE =
|
||||
ifeq ($(OS),Windows_NT)
|
||||
EXE = .exe
|
||||
endif
|
||||
|
||||
proto:
|
||||
protoc -I . $(PROTO_USED) --go_out=. --go_opt=module=lunar-tear/server --go-grpc_out=. --go-grpc_opt=module=lunar-tear/server
|
||||
$(PROTOC) -I . $(PROTO_USED) --go_out=. --go_opt=module=lunar-tear/server --go-grpc_out=. --go-grpc_opt=module=lunar-tear/server
|
||||
@echo "Generated in gen/proto/"
|
||||
|
||||
build:
|
||||
@@ -36,7 +39,7 @@ ifeq ($(OS),Windows_NT)
|
||||
else
|
||||
mkdir -p db
|
||||
endif
|
||||
goose -dir migrations -allow-missing sqlite3 db/game.db up
|
||||
$(GOOSE) -dir migrations -allow-missing sqlite3 db/game.db up
|
||||
|
||||
import:
|
||||
ifndef SNAPSHOT
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"charm.land/huh/v2/spinner"
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
const banner = `
|
||||
_ _____
|
||||
| | _ _ _ _ __ _ _ _ |_ _|___ __ _ _ _
|
||||
| |_| || | ' \/ _` + "`" + ` | '_| | |/ -_)/ _` + "`" + ` | '_|
|
||||
|____\_,_|_||_\__,_|_| |_|\___|\__,_|_|
|
||||
|
||||
`
|
||||
|
||||
const (
|
||||
configFile = ".wizard.json"
|
||||
exitVal = "__exit__"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
IP string `json:"ip"`
|
||||
Device string `json:"device"`
|
||||
Detail string `json:"detail"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupOnly := flag.Bool("setup-only", false, "show patching instructions and exit without building or launching")
|
||||
flag.Parse()
|
||||
|
||||
lipgloss.EnableLegacyWindowsANSI(os.Stdout)
|
||||
lipgloss.EnableLegacyWindowsANSI(os.Stderr)
|
||||
|
||||
fmt.Print(banner)
|
||||
|
||||
if !*setupOnly {
|
||||
validateAssets()
|
||||
validateTools()
|
||||
validateProtocIncludes()
|
||||
runProtoc()
|
||||
runMigrate()
|
||||
downloadDeps()
|
||||
}
|
||||
|
||||
ip, cfg, firstRun := resolveIP()
|
||||
|
||||
saveConfig(cfg)
|
||||
|
||||
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14)
|
||||
addrStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(ip+":8003"))
|
||||
fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(ip+":8080"))
|
||||
fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(ip+":3000"))
|
||||
fmt.Println()
|
||||
|
||||
if firstRun || *setupOnly {
|
||||
showPatcherHint(ip, !*setupOnly)
|
||||
}
|
||||
|
||||
if *setupOnly {
|
||||
return
|
||||
}
|
||||
|
||||
launchDev(ip)
|
||||
}
|
||||
|
||||
type assetCheck struct {
|
||||
path string
|
||||
dir bool
|
||||
}
|
||||
|
||||
var requiredAssets = []assetCheck{
|
||||
{"assets", true},
|
||||
{"assets/release/20240404193219.bin.e", false},
|
||||
{"assets/revisions/0/list.bin", false},
|
||||
{"assets/revisions/0/assetbundle", true},
|
||||
{"assets/revisions/0/resources", true},
|
||||
}
|
||||
|
||||
func validateAssets() {
|
||||
var missing []string
|
||||
for _, a := range requiredAssets {
|
||||
info, err := os.Stat(a.path)
|
||||
if err != nil {
|
||||
missing = append(missing, a.path)
|
||||
continue
|
||||
}
|
||||
if a.dir && !info.IsDir() {
|
||||
missing = append(missing, a.path+string(filepath.Separator))
|
||||
} else if !a.dir && info.IsDir() {
|
||||
missing = append(missing, a.path)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
errStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9"))
|
||||
pathStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
hlStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(errStyle.Render(" Required game assets are missing."))
|
||||
b.WriteString("\n\n")
|
||||
for _, p := range missing {
|
||||
b.WriteString(pathStyle.Render(" ✗ " + p))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(dimStyle.Render(" Place the extracted game assets under server/assets/ and try again."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(dimStyle.Render(" Get them from ") + hlStyle.Render("#resources") + dimStyle.Render(" on Discord: ") + hlStyle.Hyperlink("https://discord.com/invite/MZAf5aVkJG").Render("https://discord.com/invite/MZAf5aVkJG"))
|
||||
b.WriteString("\n")
|
||||
|
||||
fmt.Fprintln(os.Stderr, b.String())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
type toolReq struct {
|
||||
bin string
|
||||
install string // human-readable hint shown when tool must be installed manually
|
||||
goInstall string // `go install` package path; non-empty means auto-installable
|
||||
}
|
||||
|
||||
var requiredTools = []toolReq{
|
||||
{"make", "https://www.gnu.org/software/make/", ""},
|
||||
{"protoc", "https://protobuf.dev/installation/", ""},
|
||||
{"protoc-gen-go", "", "google.golang.org/protobuf/cmd/protoc-gen-go@latest"},
|
||||
{"protoc-gen-go-grpc", "", "google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"},
|
||||
{"goose", "", "github.com/pressly/goose/v3/cmd/goose@latest"},
|
||||
}
|
||||
|
||||
var toolPaths = map[string]string{}
|
||||
|
||||
// findTool looks for a tool on PATH first, then falls back to the current
|
||||
// directory (for Windows users who drop .exe files into server/).
|
||||
func findTool(name string) (string, error) {
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
local := name
|
||||
if runtime.GOOS == "windows" {
|
||||
local += ".exe"
|
||||
}
|
||||
if _, err := os.Stat(local); err == nil {
|
||||
abs, err := filepath.Abs(local)
|
||||
if err != nil {
|
||||
return local, nil
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s not found", name)
|
||||
}
|
||||
|
||||
func validateTools() {
|
||||
var manual []toolReq
|
||||
var installable []toolReq
|
||||
|
||||
for _, t := range requiredTools {
|
||||
if p, err := findTool(t.bin); err == nil {
|
||||
toolPaths[t.bin] = p
|
||||
} else if t.goInstall == "" {
|
||||
manual = append(manual, t)
|
||||
} else {
|
||||
installable = append(installable, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(manual) > 0 {
|
||||
errStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9"))
|
||||
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
hintStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(errStyle.Render(" Required tools are not installed."))
|
||||
b.WriteString("\n\n")
|
||||
for _, t := range manual {
|
||||
b.WriteString(nameStyle.Render(fmt.Sprintf(" ✗ %-22s", t.bin)) + hintStyle.Render(t.install))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
fmt.Fprintln(os.Stderr, b.String())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, t := range installable {
|
||||
fmt.Printf(" Installing %s...\n", t.bin)
|
||||
cmd := exec.Command("go", "install", t.goInstall)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, " Failed to install %s: %v\n", t.bin, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
p, err := findTool(t.bin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, " %s installed but not found on PATH — is $(go env GOPATH)/bin in your PATH?\n", t.bin)
|
||||
os.Exit(1)
|
||||
}
|
||||
toolPaths[t.bin] = p
|
||||
}
|
||||
}
|
||||
|
||||
func validateProtocIncludes() {
|
||||
if _, err := exec.LookPath("protoc"); err == nil {
|
||||
return
|
||||
}
|
||||
// protoc is local (not on PATH) -- verify well-known types are present.
|
||||
wkt := filepath.Join("google", "protobuf", "empty.proto")
|
||||
if _, err := os.Stat(wkt); err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
errStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9"))
|
||||
pathStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
|
||||
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(errStyle.Render(" protoc well-known types are missing."))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(pathStyle.Render(" ✗ " + wkt))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" Extract the google/ folder from the protoc release zip into server/."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(dimStyle.Render(" Download: https://github.com/protocolbuffers/protobuf/releases"))
|
||||
b.WriteString("\n")
|
||||
|
||||
fmt.Fprintln(os.Stderr, b.String())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func runProtoc() {
|
||||
_ = spinner.New().Title(" Running protoc...").Action(func() {
|
||||
runQuiet(exec.Command(toolPaths["make"], "proto", "PROTOC="+toolPaths["protoc"]), "protoc generation")
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func runMigrate() {
|
||||
_ = spinner.New().Title(" Running migrations...").Action(func() {
|
||||
runQuiet(exec.Command(toolPaths["make"], "migrate", "GOOSE="+toolPaths["goose"]), "database migration")
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func downloadDeps() {
|
||||
_ = spinner.New().Title(" Downloading dependencies...").Action(func() {
|
||||
runQuiet(exec.Command("go", "mod", "download"), "dependency download")
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func runQuiet(cmd *exec.Cmd, label string) {
|
||||
var buf strings.Builder
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprint(os.Stderr, buf.String())
|
||||
fmt.Fprintf(os.Stderr, "\n %s failed: %v\n", label, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveIP() (string, config, bool) {
|
||||
if cfg, err := loadConfig(); err == nil {
|
||||
ip, cfg, done := handleSavedConfig(cfg)
|
||||
if done {
|
||||
return ip, cfg, false
|
||||
}
|
||||
}
|
||||
|
||||
ip, cfg := runWizard()
|
||||
return ip, cfg, true
|
||||
}
|
||||
|
||||
func handleSavedConfig(cfg config) (string, config, bool) {
|
||||
reuse := true
|
||||
err := huh.NewConfirm().
|
||||
Title(fmt.Sprintf("Use same settings as last time? (%s — %s)", cfg.IP, cfg.Summary)).
|
||||
Affirmative("Yes").
|
||||
Negative("No, reconfigure").
|
||||
Value(&reuse).
|
||||
Run()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
if !reuse {
|
||||
return "", config{}, false
|
||||
}
|
||||
|
||||
if isLANBased(cfg) {
|
||||
if ip, updated, ok := recheckLANIP(cfg); ok {
|
||||
return ip, updated, true
|
||||
}
|
||||
return "", config{}, false
|
||||
}
|
||||
|
||||
return cfg.IP, cfg, true
|
||||
}
|
||||
|
||||
func isLANBased(cfg config) bool {
|
||||
if cfg.Detail == "wifi" {
|
||||
return true
|
||||
}
|
||||
switch cfg.Detail {
|
||||
case "android-studio", "bluestacks", "genymotion":
|
||||
return false
|
||||
}
|
||||
return cfg.Device == "emulator"
|
||||
}
|
||||
|
||||
func recheckLANIP(cfg config) (string, config, bool) {
|
||||
current := detectLANIP()
|
||||
if current == "" || current == cfg.IP {
|
||||
return cfg.IP, cfg, true
|
||||
}
|
||||
|
||||
var action string
|
||||
err := huh.NewSelect[string]().
|
||||
Title(fmt.Sprintf("Your LAN IP changed: %s → %s", cfg.IP, current)).
|
||||
Options(
|
||||
huh.NewOption("Use new IP ("+current+")", "update"),
|
||||
huh.NewOption("Keep saved IP ("+cfg.IP+")", "keep"),
|
||||
huh.NewOption("Reconfigure from scratch", "reconfig"),
|
||||
).
|
||||
Value(&action).
|
||||
Run()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "update":
|
||||
warnRepatch(cfg.IP, current)
|
||||
var ack bool
|
||||
_ = huh.NewConfirm().
|
||||
Title("Continue launching the server?").
|
||||
Affirmative("Yes, start").
|
||||
Negative("No, exit").
|
||||
Value(&ack).
|
||||
Run()
|
||||
if !ack {
|
||||
os.Exit(0)
|
||||
}
|
||||
cfg.IP = current
|
||||
return current, cfg, true
|
||||
case "keep":
|
||||
return cfg.IP, cfg, true
|
||||
default:
|
||||
return "", config{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func warnRepatch(oldIP, newIP string) {
|
||||
warnStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11"))
|
||||
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
hlStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
|
||||
repoURL := "https://gitlab.com/walter-sparrow-group/lunar-scripts"
|
||||
repoLink := hlStyle.Hyperlink(repoURL).Render(repoURL)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("\n")
|
||||
b.WriteString(warnStyle.Render(" ⚠ Your APK was patched for the old IP."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(dimStyle.Render(" Re-patch using ") + hlStyle.Render("lunar_tear_patcher.ipynb") + dimStyle.Render(" in ") + hlStyle.Render("Google Colab") + dimStyle.Render(":"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(" " + repoLink)
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" Update the Configuration cell with the new addresses:"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(hlStyle.Render(fmt.Sprintf(" grpc_addr = \"%s:8003\"", newIP)))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(hlStyle.Render(fmt.Sprintf(" http_addr = \"%s:8080\"", newIP)))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(hlStyle.Render(fmt.Sprintf(" auth_host = \"%s:3000\"", newIP)))
|
||||
b.WriteString("\n\n")
|
||||
fmt.Print(b.String())
|
||||
}
|
||||
|
||||
func showPatcherHint(ip string, askLaunch bool) {
|
||||
headStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11"))
|
||||
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
|
||||
hlStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
|
||||
|
||||
repoURL := "https://gitlab.com/walter-sparrow-group/lunar-scripts"
|
||||
discordURL := "https://discord.com/invite/MZAf5aVkJG"
|
||||
repoLink := hlStyle.Hyperlink(repoURL).Render(repoURL)
|
||||
discordLink := hlStyle.Hyperlink(discordURL).Render(discordURL)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(headStyle.Render(" Next step: patch your APK"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" The game client must be patched to connect to your server."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(dimStyle.Render(" Open ") + hlStyle.Render("lunar_tear_patcher.ipynb") + dimStyle.Render(" from the scripts repo in ") + hlStyle.Render("Google Colab") + dimStyle.Render(":"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(" " + repoLink)
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" Get the APK and master data links from ") + hlStyle.Render("#resources") + dimStyle.Render(" on Discord:"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(" " + discordLink)
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" Set these in the notebook's Configuration cell:"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(hlStyle.Render(fmt.Sprintf(" grpc_addr = \"%s:8003\"", ip)))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(hlStyle.Render(fmt.Sprintf(" http_addr = \"%s:8080\"", ip)))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(hlStyle.Render(fmt.Sprintf(" auth_host = \"%s:3000\"", ip)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(dimStyle.Render(" Then run all cells — a patched APK will download automatically."))
|
||||
b.WriteString("\n\n")
|
||||
fmt.Print(b.String())
|
||||
|
||||
if !askLaunch {
|
||||
return
|
||||
}
|
||||
|
||||
launch := true
|
||||
_ = huh.NewConfirm().
|
||||
Title("Launch the server?").
|
||||
Affirmative("Yes, start").
|
||||
Negative("No, exit").
|
||||
Value(&launch).
|
||||
Run()
|
||||
if !launch {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func runWizard() (string, config) {
|
||||
var device, emu, conn string
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Where is the game running?").
|
||||
Options(
|
||||
huh.NewOption("Android Emulator on this PC", "emulator"),
|
||||
huh.NewOption("Phone / Tablet on the same network", "phone"),
|
||||
huh.NewOption("Exit", exitVal),
|
||||
).
|
||||
Value(&device),
|
||||
).WithShowHelp(true),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Which emulator are you using? ").
|
||||
Options(
|
||||
huh.NewOption("Android Studio Emulator", "android-studio"),
|
||||
huh.NewOption("BlueStacks", "bluestacks"),
|
||||
huh.NewOption("Genymotion", "genymotion"),
|
||||
huh.NewOption("Nox / LDPlayer / MEmu", "nox-ld-memu"),
|
||||
huh.NewOption("Other / Not sure", "other"),
|
||||
huh.NewOption("Exit", exitVal),
|
||||
).
|
||||
Value(&emu),
|
||||
).WithHideFunc(func() bool { return device != "emulator" }).WithShowHelp(true),
|
||||
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("How is your phone connected to this PC?").
|
||||
Options(
|
||||
huh.NewOption("Same Wi-Fi network", "wifi"),
|
||||
huh.NewOption("Tailscale / ZeroTier / VPN", "vpn"),
|
||||
huh.NewOption("Something else / I'll type the IP", "manual"),
|
||||
huh.NewOption("Exit", exitVal),
|
||||
).
|
||||
Value(&conn),
|
||||
).WithHideFunc(func() bool { return device != "phone" }).WithShowHelp(true),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if device == exitVal || emu == exitVal || conn == exitVal {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return buildResult(device, emu, conn)
|
||||
}
|
||||
|
||||
func buildResult(device, emu, conn string) (string, config) {
|
||||
if device == "emulator" {
|
||||
return buildEmulatorResult(emu)
|
||||
}
|
||||
return buildPhoneResult(conn)
|
||||
}
|
||||
|
||||
func buildEmulatorResult(emu string) (string, config) {
|
||||
switch emu {
|
||||
case "android-studio", "bluestacks":
|
||||
return "10.0.2.2", config{
|
||||
IP: "10.0.2.2", Device: "emulator", Detail: emu,
|
||||
Summary: emu + " emulator",
|
||||
}
|
||||
case "genymotion":
|
||||
return "10.0.3.2", config{
|
||||
IP: "10.0.3.2", Device: "emulator", Detail: emu,
|
||||
Summary: "Genymotion emulator",
|
||||
}
|
||||
default:
|
||||
ip := detectAndConfirmIP(detectLANIP(), "emulator (LAN IP)")
|
||||
return ip, config{
|
||||
IP: ip, Device: "emulator", Detail: emu,
|
||||
Summary: emu + " emulator (LAN IP)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildPhoneResult(conn string) (string, config) {
|
||||
switch conn {
|
||||
case "wifi":
|
||||
ip := detectAndConfirmIP(detectLANIP(), "Wi-Fi")
|
||||
return ip, config{IP: ip, Device: "phone", Detail: "wifi", Summary: "phone (Wi-Fi)"}
|
||||
case "vpn":
|
||||
ip := detectAndConfirmIP(detectVPNIP(), "VPN")
|
||||
return ip, config{IP: ip, Device: "phone", Detail: "vpn", Summary: "phone (VPN)"}
|
||||
default:
|
||||
ip := promptIP("")
|
||||
return ip, config{IP: ip, Device: "phone", Detail: "manual", Summary: "phone (manual IP)"}
|
||||
}
|
||||
}
|
||||
|
||||
func detectAndConfirmIP(detected, label string) string {
|
||||
if detected == "" {
|
||||
fmt.Printf(" Could not auto-detect your %s IP address.\n", label)
|
||||
return promptIP("")
|
||||
}
|
||||
return promptIP(detected)
|
||||
}
|
||||
|
||||
func promptIP(defaultVal string) string {
|
||||
ip := defaultVal
|
||||
|
||||
title := "Enter your PC's IP address"
|
||||
if defaultVal != "" {
|
||||
title = fmt.Sprintf("Enter your PC's IP address (detected: %s)", defaultVal)
|
||||
}
|
||||
|
||||
err := huh.NewInput().
|
||||
Title(title).
|
||||
Description("Press Enter to accept, or type a different address.").
|
||||
Validate(func(s string) error {
|
||||
if net.ParseIP(strings.TrimSpace(s)) == nil {
|
||||
return fmt.Errorf("not a valid IP address")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
Value(&ip).
|
||||
Run()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return strings.TrimSpace(ip)
|
||||
}
|
||||
|
||||
func detectLANIP() string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
skipPrefixes := []string{"docker", "veth", "br-", "virbr", "tailscale", "zt", "tun", "utun"}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
name := strings.ToLower(iface.Name)
|
||||
skip := false
|
||||
for _, prefix := range skipPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip := ipNet.IP.To4()
|
||||
if ip == nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func detectVPNIP() string {
|
||||
if path, err := exec.LookPath("tailscale"); err == nil {
|
||||
out, err := exec.Command(path, "ip", "-4").Output()
|
||||
if err == nil {
|
||||
ip := strings.TrimSpace(string(out))
|
||||
if net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, tailscaleNet, _ := net.ParseCIDR("100.64.0.0/10")
|
||||
vpnPrefixes := []string{"tailscale", "zt", "tun", "utun", "wg"}
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
name := strings.ToLower(iface.Name)
|
||||
isVPN := false
|
||||
for _, prefix := range vpnPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
isVPN = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip := ipNet.IP.To4()
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
if isVPN || tailscaleNet.Contains(ip) {
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadConfig() (config, error) {
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
var cfg config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
if cfg.IP == "" {
|
||||
return config{}, fmt.Errorf("empty config")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func saveConfig(cfg config) {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(configFile, append(data, '\n'), 0644)
|
||||
}
|
||||
|
||||
func launchDev(ip string) {
|
||||
cmd := exec.Command("go", "run", "./cmd/dev",
|
||||
"--cdn.public-addr", ip+":8080",
|
||||
"--grpc.public-addr", ip+":8003",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to start dev server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
_ = cmd.Process.Signal(os.Interrupt)
|
||||
}()
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
os.Exit(exitErr.ExitCode())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
+24
-1
@@ -1,6 +1,6 @@
|
||||
module lunar-tear/server
|
||||
|
||||
go 1.25.0
|
||||
go 1.25.8
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -16,13 +16,36 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||
charm.land/huh/v2 v2.0.3 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -18,8 +50,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
@@ -28,12 +68,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
|
||||
Reference in New Issue
Block a user