Skip to content

Commit 18161e6

Browse files
committed
fix: improve github url parsing and monorepo support
1 parent b309b5e commit 18161e6

File tree

14 files changed

+1268
-154
lines changed

14 files changed

+1268
-154
lines changed

internal/app/app.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ package app
44
import (
55
"context"
66
"fmt"
7+
"os"
8+
9+
"github.com/yeasy/ask/internal/config"
710
)
811

912
// App struct
@@ -20,6 +23,44 @@ func NewApp() *App {
2023
// so we can call the runtime methods
2124
func (a *App) Startup(ctx context.Context) {
2225
a.ctx = ctx
26+
27+
// 0. Ensure global config exists
28+
globalPath := config.GetGlobalConfigPath()
29+
if _, err := os.Stat(globalPath); os.IsNotExist(err) {
30+
fmt.Println("Global config not found, initializing default...")
31+
defaultCfg := config.DefaultConfig()
32+
if err := defaultCfg.SaveGlobal(); err != nil {
33+
fmt.Printf("Failed to initialize global config: %v\n", err)
34+
} else {
35+
fmt.Printf("Initialized global config at %s\n", globalPath)
36+
}
37+
}
38+
39+
// 1. Load global config
40+
globalCfg, err := config.LoadGlobalConfig()
41+
if err == nil && globalCfg.LastProjectRoot != "" {
42+
// 2. Try to switch to LastProjectRoot
43+
if err := os.Chdir(globalCfg.LastProjectRoot); err != nil {
44+
fmt.Printf("Failed to switch to last project root: %v\n", err)
45+
// If failed, fall back to home
46+
fallbackToHome()
47+
} else {
48+
fmt.Printf("Restored project root: %s\n", globalCfg.LastProjectRoot)
49+
}
50+
} else {
51+
// 3. Default to user home if no last root or config load failed
52+
fallbackToHome()
53+
}
54+
}
55+
56+
func fallbackToHome() {
57+
if home, err := os.UserHomeDir(); err == nil {
58+
if err := os.Chdir(home); err != nil {
59+
fmt.Printf("Failed to switch to home dir: %v\n", err)
60+
} else {
61+
fmt.Printf("Switched to home dir: %s\n", home)
62+
}
63+
}
2364
}
2465

2566
// Greet returns a greeting for the given name

internal/config/config.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ type SkillInfo struct {
7777

7878
// Config represents the structure of ask.yaml
7979
type Config struct {
80-
Version string `yaml:"version"`
81-
SkillsDir string `yaml:"skills_dir,omitempty"` // Skills installation directory (default: .agent/skills)
82-
ToolTargets []ToolTarget `yaml:"tool_targets,omitempty"` // Target AI tools for skill installation
83-
Skills []string `yaml:"skills,omitempty"` // Legacy: simple list of skill names
84-
SkillsInfo []SkillInfo `yaml:"skills_info,omitempty"` // New: skills with metadata
85-
Repos []Repo `yaml:"repos,omitempty"`
80+
Version string `yaml:"version"`
81+
SkillsDir string `yaml:"skills_dir,omitempty"` // Skills installation directory (default: .agent/skills)
82+
ToolTargets []ToolTarget `yaml:"tool_targets,omitempty"` // Target AI tools for skill installation
83+
Skills []string `yaml:"skills,omitempty"` // Legacy: simple list of skill names
84+
SkillsInfo []SkillInfo `yaml:"skills_info,omitempty"` // New: skills with metadata
85+
Repos []Repo `yaml:"repos,omitempty"`
86+
LastProjectRoot string `yaml:"last_project_root,omitempty"` // Last used project root (global only)
8687
}
8788

8889
// DefaultSkillsDir is the default directory to install skills

internal/github/utils.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package github
22

33
import (
4+
"fmt"
45
"path/filepath"
56
"strings"
67
)
@@ -42,3 +43,44 @@ func ParseBrowserURL(url string) (repoURL, branch, subDir, skillName string, ok
4243

4344
return repoURL, branch, subDir, skillName, true
4445
}
46+
47+
// ParseRepoURL parses a GitHub repository URL to extract owner and repo name
48+
// Supports formats:
49+
// - owner/repo
50+
// - https://github.com/owner/repo
51+
// - https://github.com/owner/repo.git
52+
// - git@github.com:owner/repo.git
53+
func ParseRepoURL(url string) (owner, repo string, err error) {
54+
url = strings.TrimSpace(url)
55+
url = strings.TrimSuffix(url, "/")
56+
url = strings.TrimSuffix(url, ".git")
57+
58+
// Handle git@github.com:owner/repo
59+
if strings.HasPrefix(url, "git@") {
60+
parts := strings.Split(url, ":")
61+
if len(parts) != 2 {
62+
return "", "", fmt.Errorf("invalid git url: %s", url)
63+
}
64+
url = parts[1]
65+
}
66+
67+
// Handle https://github.com/owner/repo
68+
if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
69+
// Just strip protocol and domain
70+
parts := strings.Split(url, "github.com/")
71+
if len(parts) == 2 {
72+
url = parts[1]
73+
} else {
74+
// If we couldn't strip github.com from an http(s) URL, it's not a valid GitHub repo URL for us
75+
return "", "", fmt.Errorf("invalid repo URL (must be github.com): %s", url)
76+
}
77+
}
78+
79+
// Split owner/repo
80+
parts := strings.Split(url, "/")
81+
if len(parts) >= 2 {
82+
return parts[0], parts[1], nil
83+
}
84+
85+
return "", "", fmt.Errorf("invalid repo format (expected owner/repo): %s", url)
86+
}

internal/github/utils_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,105 @@ func TestParseBrowserURL(t *testing.T) {
9595
})
9696
}
9797
}
98+
99+
func TestParseRepoURL(t *testing.T) {
100+
tests := []struct {
101+
name string
102+
input string
103+
wantOwner string
104+
wantRepo string
105+
wantErr bool
106+
}{
107+
{
108+
name: "simple owner/repo",
109+
input: "owner/repo",
110+
wantOwner: "owner",
111+
wantRepo: "repo",
112+
wantErr: false,
113+
},
114+
{
115+
name: "https url",
116+
input: "https://github.com/owner/repo",
117+
wantOwner: "owner",
118+
wantRepo: "repo",
119+
wantErr: false,
120+
},
121+
{
122+
name: "https url with .git",
123+
input: "https://github.com/owner/repo.git",
124+
wantOwner: "owner",
125+
wantRepo: "repo",
126+
wantErr: false,
127+
},
128+
{
129+
name: "git ssh url",
130+
input: "git@github.com:owner/repo.git",
131+
wantOwner: "owner",
132+
wantRepo: "repo",
133+
wantErr: false,
134+
},
135+
{
136+
name: "trailing slash",
137+
input: "https://github.com/owner/repo/",
138+
wantOwner: "owner",
139+
wantRepo: "repo",
140+
wantErr: false,
141+
},
142+
{
143+
name: "url with subpath",
144+
input: "https://github.com/owner/repo/tree/main",
145+
wantOwner: "owner",
146+
wantRepo: "repo",
147+
wantErr: false,
148+
},
149+
{
150+
name: "url with deep link",
151+
input: "https://github.com/owner/repo/blob/master/README.md",
152+
wantOwner: "owner",
153+
wantRepo: "repo",
154+
wantErr: false,
155+
},
156+
{
157+
name: "short format with subpath",
158+
input: "owner/repo/path/to/skill",
159+
wantOwner: "owner",
160+
wantRepo: "repo",
161+
wantErr: false,
162+
},
163+
{
164+
name: "whitespace",
165+
input: " owner/repo ",
166+
wantOwner: "owner",
167+
wantRepo: "repo",
168+
wantErr: false,
169+
},
170+
{
171+
name: "invalid format",
172+
input: "repoonly",
173+
wantErr: true,
174+
},
175+
{
176+
name: "invalid https",
177+
input: "https://google.com",
178+
wantErr: true,
179+
},
180+
}
181+
182+
for _, tt := range tests {
183+
t.Run(tt.name, func(t *testing.T) {
184+
gotOwner, gotRepo, err := ParseRepoURL(tt.input)
185+
if (err != nil) != tt.wantErr {
186+
t.Errorf("ParseRepoURL() error = %v, wantErr %v", err, tt.wantErr)
187+
return
188+
}
189+
if !tt.wantErr {
190+
if gotOwner != tt.wantOwner {
191+
t.Errorf("ParseRepoURL() owner = %v, want %v", gotOwner, tt.wantOwner)
192+
}
193+
if gotRepo != tt.wantRepo {
194+
t.Errorf("ParseRepoURL() repo = %v, want %v", gotRepo, tt.wantRepo)
195+
}
196+
}
197+
})
198+
}
199+
}

0 commit comments

Comments
 (0)