From a9162d3bfaf006917ba4a06e99e1702fd098c88e Mon Sep 17 00:00:00 2001 From: Jordan Coin Jackson Date: Thu, 1 Jan 2026 10:45:36 -0500 Subject: [PATCH 1/2] Add remote repo support: codemap github.com/user/repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect GitHub/GitLab URLs and clone to temp dir - Show repo name (e.g., "charmbracelet/gum") instead of temp dir - Add source indicator line: ↳ github.com/charmbracelet/gum - Quiet git clone (no output noise) - Auto-cleanup temp dir after rendering Usage: codemap github.com/charmbracelet/gum codemap github.com/JordanCoin/codemap --depth 2 šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- main.go | 84 ++++++++++++++++++++++++++++++++++++++++++----- render/skyline.go | 5 ++- render/tree.go | 11 ++++++- scanner/types.go | 20 ++++++----- 4 files changed, 100 insertions(+), 20 deletions(-) diff --git a/main.go b/main.go index aed252b..78500a6 100644 --- a/main.go +++ b/main.go @@ -112,6 +112,22 @@ func main() { root = "." } + // Handle GitHub URLs - clone to temp dir + var tempDir string + var remoteURL, repoName string + if isGitHubURL(root) { + remoteURL = root + repoName = extractRepoName(root) + var err error + tempDir, err = cloneRepo(root) + if err != nil { + fmt.Fprintf(os.Stderr, "Error cloning repo: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + root = tempDir + } + absRoot, err := filepath.Abs(root) if err != nil { fmt.Fprintf(os.Stderr, "Error getting absolute path: %v\n", err) @@ -204,15 +220,17 @@ func main() { } project := scanner.Project{ - Root: absRoot, - Mode: mode, - Animate: *animateMode, - Files: files, - DiffRef: activeDiffRef, - Impact: impact, - Depth: *depthLimit, - Only: only, - Exclude: exclude, + Root: absRoot, + Name: repoName, + RemoteURL: remoteURL, + Mode: mode, + Animate: *animateMode, + Files: files, + DiffRef: activeDiffRef, + Impact: impact, + Depth: *depthLimit, + Only: only, + Exclude: exclude, } // Render or output JSON @@ -440,3 +458,51 @@ func runDaemon(root string) { daemon.Stop() watch.RemovePID(root) } + +// isGitHubURL checks if the input looks like a GitHub repo URL +func isGitHubURL(s string) bool { + s = strings.ToLower(s) + return strings.HasPrefix(s, "github.com/") || + strings.HasPrefix(s, "https://github.com/") || + strings.HasPrefix(s, "http://github.com/") || + strings.HasPrefix(s, "gitlab.com/") || + strings.HasPrefix(s, "https://gitlab.com/") +} + +// cloneRepo clones a git repo to a temp directory (shallow clone) +func cloneRepo(url string) (string, error) { + // Normalize URL + if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { + url = "https://" + url + } + + // Create temp dir + tempDir, err := os.MkdirTemp("", "codemap-") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + // Shallow clone (quiet) + cmd := exec.Command("git", "clone", "--depth", "1", "--single-branch", "-q", url, tempDir) + if err := cmd.Run(); err != nil { + os.RemoveAll(tempDir) + return "", fmt.Errorf("git clone failed: %w", err) + } + + return tempDir, nil +} + +// extractRepoName extracts "owner/repo" from a GitHub URL +func extractRepoName(url string) string { + // Remove protocol + url = strings.TrimPrefix(url, "https://") + url = strings.TrimPrefix(url, "http://") + // Remove host + url = strings.TrimPrefix(url, "github.com/") + url = strings.TrimPrefix(url, "gitlab.com/") + // Remove trailing .git + url = strings.TrimSuffix(url, ".git") + // Remove trailing slashes + url = strings.TrimSuffix(url, "/") + return url +} diff --git a/render/skyline.go b/render/skyline.go index 20cd9f9..ee9ee0b 100644 --- a/render/skyline.go +++ b/render/skyline.go @@ -209,7 +209,10 @@ func createBuildings(sorted []extAgg, width int) []building { // Skyline renders the city skyline visualization func Skyline(project scanner.Project, animate bool) { files := project.Files - projectName := filepath.Base(project.Root) + projectName := project.Name + if projectName == "" { + projectName = filepath.Base(project.Root) + } width, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil || width <= 0 { diff --git a/render/tree.go b/render/tree.go index dfd78d3..023c07b 100644 --- a/render/tree.go +++ b/render/tree.go @@ -106,7 +106,10 @@ func formatSize(size int64) string { // Tree renders the file tree to stdout func Tree(project scanner.Project) { files := project.Files - projectName := filepath.Base(project.Root) + projectName := project.Name + if projectName == "" { + projectName = filepath.Base(project.Root) + } isDiffMode := project.DiffRef != "" maxDepth := project.Depth // 0 = unlimited @@ -188,6 +191,12 @@ func Tree(project scanner.Project) { fmt.Printf("│ %-*s │\n", innerWidth-2, extLine) } + // Remote URL indicator + if project.RemoteURL != "" { + remoteLine := fmt.Sprintf("↳ %s", project.RemoteURL) + fmt.Printf("│ %-*s │\n", innerWidth-2, remoteLine) + } + fmt.Printf("ā•°%s╯\n", strings.Repeat("─", innerWidth)) // Build and render tree diff --git a/scanner/types.go b/scanner/types.go index ad31b89..15dc988 100644 --- a/scanner/types.go +++ b/scanner/types.go @@ -17,15 +17,17 @@ type FileInfo struct { // Project represents the root of the codebase for tree/skyline mode. type Project struct { - Root string `json:"root"` - Mode string `json:"mode"` - Animate bool `json:"animate"` - Files []FileInfo `json:"files"` - DiffRef string `json:"diff_ref,omitempty"` - Impact []ImpactInfo `json:"impact,omitempty"` - Depth int `json:"depth,omitempty"` // Max tree depth (0 = unlimited) - Only []string `json:"only,omitempty"` // Extension filter (e.g., ["swift", "go"]) - Exclude []string `json:"exclude,omitempty"` // Exclusion patterns (e.g., [".xcassets", "Fonts"]) + Root string `json:"root"` + Name string `json:"name,omitempty"` // Display name (overrides filepath.Base(Root)) + RemoteURL string `json:"remote_url,omitempty"` // Source URL if cloned from remote + Mode string `json:"mode"` + Animate bool `json:"animate"` + Files []FileInfo `json:"files"` + DiffRef string `json:"diff_ref,omitempty"` + Impact []ImpactInfo `json:"impact,omitempty"` + Depth int `json:"depth,omitempty"` // Max tree depth (0 = unlimited) + Only []string `json:"only,omitempty"` // Extension filter (e.g., ["swift", "go"]) + Exclude []string `json:"exclude,omitempty"` // Exclusion patterns (e.g., [".xcassets", "Fonts"]) } // FileAnalysis holds extracted info about a single file for deps mode. From 17028a4641e39c0d96b080d6dde27ae35a20ae0d Mon Sep 17 00:00:00 2001 From: Jordan Coin Jackson Date: Thu, 1 Jan 2026 10:57:43 -0500 Subject: [PATCH 2/2] Prefer local paths over remote clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a path like github.com/user/repo exists locally (e.g., ~/go/src/github.com/...), use the local path instead of cloning from remote. Only clone when the path doesn't exist locally. šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- main.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 78500a6..6a41ef7 100644 --- a/main.go +++ b/main.go @@ -112,10 +112,13 @@ func main() { root = "." } - // Handle GitHub URLs - clone to temp dir + // Handle GitHub URLs - clone to temp dir (but prefer local paths if they exist) var tempDir string var remoteURL, repoName string - if isGitHubURL(root) { + _, localPathErr := os.Stat(root) + if isGitHubURL(root) && localPathErr != nil { + // Only clone if it looks like a URL AND doesn't exist locally + // This preserves ~/go/src/github.com/user/repo style paths remoteURL = root repoName = extractRepoName(root) var err error