Skip to content

Commit e10fe80

Browse files
authored
Add hooks-first onboarding setup flow (#33)
* Add hooks-first onboarding setup command and docs * Address Copilot setup review feedback * Document repo-root requirement for hooks and setup
1 parent add5270 commit e10fe80

9 files changed

Lines changed: 694 additions & 64 deletions

File tree

CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# 🛑 STOP — Run codemap before ANY task
22

3+
## Repo Root Requirement (Critical)
4+
5+
Run codemap from the git repository root. Hooks and context files resolve from the current working directory, so running from a subdirectory can break hook context.
6+
7+
```bash
8+
cd "$(git rev-parse --show-toplevel)"
9+
```
10+
11+
`codemap` expects these at repo root:
12+
- `.git/`
13+
- `.codemap/`
14+
- `.claude/settings.local.json` (project-local hooks)
15+
316
```bash
417
codemap . # Project structure
518
codemap --deps # How files connect

README.md

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,66 @@ scoop install codemap
2424

2525
> Other options: [Releases](https://github.com/JordanCoin/codemap/releases) | `go install` | Build from source
2626
27-
## Quick Start
27+
## Recommended Setup (Hooks + Daemon + Config)
28+
29+
No repo clone is required for normal users.
30+
Run setup from your git repo root (not a subdirectory), or hooks may not resolve project context.
31+
32+
```bash
33+
# install codemap first (package manager)
34+
brew tap JordanCoin/tap && brew install codemap
35+
36+
# then run setup inside your project
37+
cd /path/to/your/project
38+
codemap setup
39+
```
40+
41+
`codemap setup` is the default onboarding path and configures the pieces that make codemap most useful with Claude:
42+
- creates `.codemap/config.json` (if missing) with auto-detected language filters
43+
- installs codemap hooks into `.claude/settings.local.json` (project-local by default)
44+
- hooks automatically start/read daemon state on session start
45+
46+
Use global Claude settings instead of project-local settings:
47+
48+
```bash
49+
codemap setup --global
50+
```
51+
52+
Windows equivalent:
53+
54+
```bash
55+
scoop bucket add codemap https://github.com/JordanCoin/scoop-codemap
56+
scoop install codemap
57+
cd C:\path\to\your\project
58+
codemap setup
59+
```
60+
61+
Optional helper scripts (mainly for contributors running from this repo):
62+
- macOS/Linux: `./scripts/onboard.sh /path/to/your/project`
63+
- Windows (PowerShell): `./scripts/onboard.ps1 -ProjectRoot C:\path\to\your\project`
64+
65+
## Verify Setup
66+
67+
1. Restart Claude Code or open a new session.
68+
2. At session start, you should see codemap project context.
69+
3. Edit a file and confirm pre/post edit hook context appears.
70+
71+
## Daily Commands
72+
73+
```bash
74+
codemap . # Fast tree/context view (respects .codemap/config.json)
75+
codemap --diff # What changed vs main
76+
codemap handoff . # Save layered handoff for cross-agent continuation
77+
codemap --deps . # Dependency flow (requires ast-grep)
78+
```
79+
80+
## Other Commands
2881

2982
```bash
30-
codemap . # Project tree
31-
codemap --only swift . # Just Swift files
32-
codemap --exclude .xcassets,Fonts,.png . # Hide assets
33-
codemap --depth 2 . # Limit depth
34-
codemap --diff # What changed vs main
35-
codemap --deps . # Dependency flow
36-
codemap config init # Create .codemap/config.json
37-
codemap handoff . # Save cross-agent handoff summary
38-
codemap github.com/user/repo # Remote GitHub repo
83+
codemap --only swift .
84+
codemap --exclude .xcassets,Fonts,.png .
85+
codemap --depth 2 .
86+
codemap github.com/user/repo
3987
```
4088

4189
## Options

cmd/config.go

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -12,6 +13,15 @@ import (
1213
"codemap/scanner"
1314
)
1415

16+
var errConfigExists = errors.New("config already exists")
17+
18+
type configInitResult struct {
19+
Path string
20+
TopExts []string
21+
TotalFiles int
22+
MatchedFiles int
23+
}
24+
1525
// nonCodeExtensions are extensions excluded from "config init" auto-detection.
1626
// These are documentation, data, or lock files that rarely represent the
1727
// project's primary code.
@@ -46,106 +56,107 @@ func RunConfig(subCmd, root string) {
4656
}
4757

4858
func configInit(root string) {
49-
cfgPath := config.ConfigPath(root)
50-
51-
// Warn if config already exists
52-
if _, err := os.Stat(cfgPath); err == nil {
59+
result, err := initProjectConfig(root)
60+
if errors.Is(err, errConfigExists) {
61+
cfgPath := config.ConfigPath(root)
5362
fmt.Fprintf(os.Stderr, "Config already exists: %s\n", cfgPath)
5463
fmt.Fprintln(os.Stderr, "Use 'codemap config show' to view it, or edit directly.")
5564
os.Exit(1)
5665
}
66+
if err != nil {
67+
fmt.Fprintf(os.Stderr, "Error creating config: %v\n", err)
68+
os.Exit(1)
69+
}
70+
71+
fmt.Printf("Created %s\n", result.Path)
72+
fmt.Println()
73+
if len(result.TopExts) == 0 {
74+
fmt.Println("No code extensions detected — wrote empty config.")
75+
} else {
76+
fmt.Printf(" only: %s\n", strings.Join(result.TopExts, ", "))
77+
if result.TotalFiles > 0 {
78+
fmt.Printf(" (%d of %d files)\n", result.MatchedFiles, result.TotalFiles)
79+
}
80+
}
81+
fmt.Println()
82+
fmt.Println("Edit the file to add 'exclude' patterns or adjust 'depth'.")
83+
}
84+
85+
func initProjectConfig(root string) (configInitResult, error) {
86+
cfgPath := config.ConfigPath(root)
87+
result := configInitResult{Path: cfgPath}
88+
89+
if _, err := os.Stat(cfgPath); err == nil {
90+
return result, errConfigExists
91+
} else if err != nil && !os.IsNotExist(err) {
92+
return result, err
93+
}
5794

58-
// Scan the repo to find top extensions
5995
gitCache := scanner.NewGitIgnoreCache(root)
6096
files, err := scanner.ScanFiles(root, gitCache, nil, nil)
6197
if err != nil {
62-
fmt.Fprintf(os.Stderr, "Error scanning files: %v\n", err)
63-
os.Exit(1)
98+
return result, fmt.Errorf("scan files: %w", err)
6499
}
65100

66-
// Count extensions
67101
extCount := make(map[string]int)
68102
for _, f := range files {
69-
ext := strings.TrimPrefix(f.Ext, ".")
70-
if ext == "" {
71-
continue
72-
}
73-
ext = strings.ToLower(ext)
74-
if nonCodeExtensions[ext] {
103+
ext := strings.TrimPrefix(strings.ToLower(f.Ext), ".")
104+
if ext == "" || nonCodeExtensions[ext] {
75105
continue
76106
}
77107
extCount[ext]++
78108
}
79109

80-
// Sort by frequency
81110
type extEntry struct {
82111
Ext string
83112
Count int
84113
}
85114
var entries []extEntry
86115
for ext, count := range extCount {
87-
entries = append(entries, extEntry{ext, count})
116+
entries = append(entries, extEntry{Ext: ext, Count: count})
88117
}
89118
sort.Slice(entries, func(i, j int) bool {
90119
return entries[i].Count > entries[j].Count
91120
})
92121

93-
// Take top 5
94-
var topExts []string
95122
for i, e := range entries {
96123
if i >= 5 {
97124
break
98125
}
99-
topExts = append(topExts, e.Ext)
126+
result.TopExts = append(result.TopExts, e.Ext)
100127
}
101128

102-
if len(topExts) == 0 {
103-
fmt.Println("No code extensions detected — writing empty config.")
104-
topExts = nil
105-
}
129+
cfg := config.ProjectConfig{Only: result.TopExts}
106130

107-
cfg := config.ProjectConfig{
108-
Only: topExts,
109-
}
110-
111-
// Ensure .codemap/ directory exists
112131
if err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil {
113-
fmt.Fprintf(os.Stderr, "Error creating directory: %v\n", err)
114-
os.Exit(1)
132+
return result, fmt.Errorf("create .codemap directory: %w", err)
115133
}
116134

117135
data, err := json.MarshalIndent(cfg, "", " ")
118136
if err != nil {
119-
fmt.Fprintf(os.Stderr, "Error encoding config: %v\n", err)
120-
os.Exit(1)
137+
return result, fmt.Errorf("encode config: %w", err)
121138
}
122139
data = append(data, '\n')
123140

124141
if err := os.WriteFile(cfgPath, data, 0644); err != nil {
125-
fmt.Fprintf(os.Stderr, "Error writing config: %v\n", err)
126-
os.Exit(1)
142+
return result, fmt.Errorf("write config: %w", err)
127143
}
128144

129-
fmt.Printf("Created %s\n", cfgPath)
130-
fmt.Println()
131-
fmt.Printf(" only: %s\n", strings.Join(topExts, ", "))
132-
if len(files) > 0 && len(topExts) > 0 {
133-
// Count how many files match the selected extensions
134-
matchExts := make(map[string]bool)
135-
for _, ext := range topExts {
145+
result.TotalFiles = len(files)
146+
if len(result.TopExts) > 0 {
147+
matchExts := make(map[string]bool, len(result.TopExts))
148+
for _, ext := range result.TopExts {
136149
matchExts[ext] = true
137150
}
138-
matched := 0
139151
for _, f := range files {
140152
ext := strings.TrimPrefix(strings.ToLower(f.Ext), ".")
141153
if matchExts[ext] {
142-
matched++
154+
result.MatchedFiles++
143155
}
144156
}
145-
fmt.Printf(" (%d of %d files)\n", matched, len(files))
146157
}
147-
fmt.Println()
148-
fmt.Println("Edit the file to add 'exclude' patterns or adjust 'depth'.")
158+
159+
return result, nil
149160
}
150161

151162
func configShow(root string) {

0 commit comments

Comments
 (0)