File size: 2,953 Bytes
6a7089a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package config

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

var defaultLocalAllowedDomains = []string{"127.0.0.1", "localhost", "::1"}

func homeDir() string {
	if home := os.Getenv("HOME"); home != "" {
		return home
	}
	h, _ := os.UserHomeDir()
	return h
}

func userConfigDir() string {
	home := homeDir()
	legacyPath := filepath.Join(home, ".pinchtab")

	configDir, err := os.UserConfigDir()
	if err != nil {
		return legacyPath
	}

	newPath := filepath.Join(configDir, "pinchtab")

	// Priority 1: Check for config FILE (handles case where both dirs exist
	// but only legacy has config.json — the issue #224 scenario)
	legacyConfig := filepath.Join(legacyPath, "config.json")
	newConfig := filepath.Join(newPath, "config.json")

	if fileExists(legacyConfig) && !fileExists(newConfig) {
		return legacyPath
	}

	// Priority 2: Check for DIRECTORY (handles init scenario where
	// legacy dir exists from npm install but no config yet)
	if dirExists(legacyPath) && !dirExists(newPath) {
		return legacyPath
	}

	return newPath
}

func fileExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && !info.IsDir()
}

// DefaultConfigPath returns the default config file location used when
// PINCHTAB_CONFIG is not explicitly set.
func DefaultConfigPath() string {
	return filepath.Join(userConfigDir(), "config.json")
}

func dirExists(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}
	return info.IsDir()
}

func (c *RuntimeConfig) ListenAddr() string {
	return c.Bind + ":" + c.Port
}

func GenerateAuthToken() (string, error) {
	buf := make([]byte, 24)
	if _, err := rand.Read(buf); err != nil {
		return "", fmt.Errorf("generate auth token: %w", err)
	}
	return hex.EncodeToString(buf), nil
}

func MaskToken(t string) string {
	if t == "" {
		return "(none)"
	}
	if len(t) <= 8 {
		return "***"
	}
	return t[:4] + "..." + t[len(t)-4:]
}

// NeedsWizard returns true if the config has no version or an older version than current.
func NeedsWizard(cfg *FileConfig) bool {
	if cfg.ConfigVersion == "" {
		return true
	}
	return CompareVersions(cfg.ConfigVersion, CurrentConfigVersion) < 0
}

// IsFirstRun returns true if the config has never been through the wizard.
func IsFirstRun(cfg *FileConfig) bool {
	return cfg.ConfigVersion == ""
}

// CompareVersions compares two semver-like version strings.
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
func CompareVersions(a, b string) int {
	aParts := splitVersion(a)
	bParts := splitVersion(b)
	for i := 0; i < 3; i++ {
		if aParts[i] < bParts[i] {
			return -1
		}
		if aParts[i] > bParts[i] {
			return 1
		}
	}
	return 0
}

func splitVersion(v string) [3]int {
	parts := [3]int{}
	segs := strings.SplitN(v, ".", 3)
	for i, s := range segs {
		if i >= 3 {
			break
		}
		n := 0
		for _, c := range s {
			if c >= '0' && c <= '9' {
				n = n*10 + int(c-'0')
			}
		}
		parts[i] = n
	}
	return parts
}