-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpairing.go
More file actions
318 lines (272 loc) · 7.86 KB
/
Copy pathpairing.go
File metadata and controls
318 lines (272 loc) · 7.86 KB
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
)
// PairedMobile represents a mobile device paired with this PC
type PairedMobile struct {
ID string `json:"id"`
Name string `json:"name"`
PublicKey string `json:"public_key"`
PairedAt string `json:"paired_at"`
}
// PCConfig represents the PC's identity and paired devices
type PCConfig struct {
PCID string `json:"pc_id"`
PCName string `json:"pc_name"`
PrivateKey string `json:"private_key"`
PublicKey string `json:"public_key"`
Secret string `json:"secret,omitempty"`
PairedMobiles []PairedMobile `json:"paired_mobiles"`
CreatedAt string `json:"created_at"`
}
// DirectoryConfig represents remembered agent choice per directory
type DirectoryConfig struct {
DefaultAgent string `json:"default_agent"`
LastUsed string `json:"last_used"`
}
// DirectoriesConfig maps directory paths to their config
type DirectoriesConfig map[string]DirectoryConfig
// customConfigDir overrides the default config directory when set via --config-dir
var customConfigDir string
// getConfigDir returns the aipilot config directory path
func getConfigDir() (string, error) {
if customConfigDir != "" {
return customConfigDir, nil
}
configDir, err := os.UserConfigDir()
if err != nil {
// Fallback to home directory
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("cannot determine config directory: %w", err)
}
configDir = filepath.Join(home, ".config")
}
return filepath.Join(configDir, "aipilot"), nil
}
// ensureConfigDir creates the config directory if it doesn't exist
func ensureConfigDir() (string, error) {
dir, err := getConfigDir()
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, DirPermissions); err != nil {
return "", fmt.Errorf("failed to create config directory: %w", err)
}
return dir, nil
}
// loadPCConfig loads the PC configuration
func loadPCConfig() (*PCConfig, error) {
dir, err := getConfigDir()
if err != nil {
return nil, err
}
path := filepath.Join(dir, "config.json")
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil // No config yet
}
return nil, err
}
var config PCConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
// savePCConfig saves the PC configuration
func savePCConfig(config *PCConfig) error {
dir, err := ensureConfigDir()
if err != nil {
return err
}
path := filepath.Join(dir, "config.json")
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, FilePermissions)
}
// createPCConfig creates a new PC configuration with generated keys
func createPCConfig() (*PCConfig, error) {
// Generate X25519 key pair for NaCl box encryption
priv, pub, err := GenerateX25519KeyPair()
if err != nil {
return nil, fmt.Errorf("failed to generate key pair: %w", err)
}
// Get hostname for PC name
hostname, err := os.Hostname()
if err != nil {
hostname = "Unknown PC"
}
config := &PCConfig{
PCID: uuid.New().String(),
PCName: hostname,
PrivateKey: hex.EncodeToString(priv[:]),
PublicKey: hex.EncodeToString(pub[:]),
PairedMobiles: []PairedMobile{},
CreatedAt: time.Now().Format(time.RFC3339),
}
if err := savePCConfig(config); err != nil {
return nil, err
}
return config, nil
}
// getOrCreatePCConfig loads existing config or creates a new one
func getOrCreatePCConfig() (*PCConfig, error) {
config, err := loadPCConfig()
if err != nil {
return nil, err
}
if config == nil {
return createPCConfig()
}
return config, nil
}
// hasPairedMobiles returns true if at least one mobile is paired
func (c *PCConfig) hasPairedMobiles() bool {
return len(c.PairedMobiles) > 0
}
// getPairedMobile returns a paired mobile by ID, or nil if not found
func (c *PCConfig) getPairedMobile(mobileID string) *PairedMobile {
for i := range c.PairedMobiles {
if c.PairedMobiles[i].ID == mobileID {
return &c.PairedMobiles[i]
}
}
return nil
}
// addPairedMobile adds a new paired mobile
func (c *PCConfig) addPairedMobile(mobile PairedMobile) {
// Check if already exists
for i, m := range c.PairedMobiles {
if m.ID == mobile.ID {
// Update existing
c.PairedMobiles[i] = mobile
return
}
}
c.PairedMobiles = append(c.PairedMobiles, mobile)
}
// removePairedMobile removes a paired mobile by ID
func (c *PCConfig) removePairedMobile(mobileID string) bool {
for i, m := range c.PairedMobiles {
if m.ID == mobileID {
c.PairedMobiles = append(c.PairedMobiles[:i], c.PairedMobiles[i+1:]...)
return true
}
}
return false
}
// loadDirectoriesConfig loads the directories configuration
func loadDirectoriesConfig() (DirectoriesConfig, error) {
dir, err := getConfigDir()
if err != nil {
return nil, err
}
path := filepath.Join(dir, "directories.json")
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return make(DirectoriesConfig), nil
}
return nil, err
}
var config DirectoriesConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return config, nil
}
// saveDirectoriesConfig saves the directories configuration
func saveDirectoriesConfig(config DirectoriesConfig) error {
dir, err := ensureConfigDir()
if err != nil {
return err
}
path := filepath.Join(dir, "directories.json")
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, FilePermissions)
}
// getDirectoryAgent returns the default agent for a directory
func getDirectoryAgent(workDir string) string {
config, err := loadDirectoriesConfig()
if err != nil {
return ""
}
if dc, ok := config[workDir]; ok {
return dc.DefaultAgent
}
return ""
}
// setDirectoryAgent saves the default agent for a directory
func setDirectoryAgent(workDir, agent string) error {
config, err := loadDirectoriesConfig()
if err != nil {
return err
}
config[workDir] = DirectoryConfig{
DefaultAgent: agent,
LastUsed: time.Now().Format(time.RFC3339),
}
return saveDirectoriesConfig(config)
}
// PairingQRData is the data encoded in the pairing QR code
type PairingQRData struct {
Type string `json:"type"` // "pairing"
Relay string `json:"r"`
Token string `json:"t"`
PCID string `json:"pc"`
PCName string `json:"n"`
PublicKey string `json:"k"`
// Optional: session info for immediate display (backup if notification fails)
SessionID string `json:"s,omitempty"`
WorkingDir string `json:"wd,omitempty"`
AgentType string `json:"at,omitempty"`
SSHAvailable bool `json:"sa,omitempty"`
SSHPort int `json:"sp,omitempty"`
Hostname string `json:"h,omitempty"`
Username string `json:"u,omitempty"`
}
// SessionQRInfo holds optional session-specific data for the pairing QR code.
// When non-nil, session info and SSH detection results are included in the QR.
type SessionQRInfo struct {
SessionID string
WorkDir string
AgentType string
}
// buildPairingQRData constructs the PairingQRData struct used for QR code generation.
// sessionInfo is optional (nil when pairing before a session exists).
func buildPairingQRData(config *PCConfig, relayURL, pairingToken string, sessionInfo *SessionQRInfo) PairingQRData {
qrData := PairingQRData{
Type: "pairing",
Relay: relayURL,
Token: pairingToken,
PCID: config.PCID,
PCName: config.PCName,
PublicKey: config.PublicKey,
}
if sessionInfo != nil {
qrData.SessionID = sessionInfo.SessionID
qrData.WorkingDir = sessionInfo.WorkDir
qrData.AgentType = sessionInfo.AgentType
sshInfo := DetectSSHInfo()
if sshInfo != nil && sshInfo.Available {
qrData.SSHAvailable = true
qrData.SSHPort = sshInfo.Port
qrData.Hostname = sshInfo.Hostname
qrData.Username = sshInfo.Username
}
}
return qrData
}