fix: resolve first-run experience errors (#721, #771, #730)

- cmdInit (--init) now substitutes EDITOR_PATH, PAGER_PATH, and
  WORK_PATH instead of leaving them as literal strings
- Installer now substitutes WORK_PATH and always creates personal
  and work directories regardless of community cheatsheet choice
- When community cheatsheets are declined, the community cheatpath
  is commented out in the generated config
- config.New() skips nonexistent cheatpaths with a warning instead
  of hard-erroring on EvalSymlinks failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Allen Lane
2026-02-14 20:42:33 -05:00
parent 8eafa5adfe
commit 00ec2c130d
5 changed files with 74 additions and 20 deletions

View File

@@ -44,13 +44,23 @@ func cmdInit() {
confpath := confpaths[0]
confdir := filepath.Dir(confpath)
// create paths for community and personal cheatsheets
// create paths for community, personal, and work cheatsheets
community := filepath.Join(confdir, "cheatsheets", "community")
personal := filepath.Join(confdir, "cheatsheets", "personal")
work := filepath.Join(confdir, "cheatsheets", "work")
// template the above paths into the default configs
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
configs = strings.Replace(configs, "WORK_PATH", work, -1)
// locate and set a default pager
configs = strings.Replace(configs, "PAGER_PATH", config.Pager(), -1)
// locate and set a default editor
if editor, err := config.Editor(); err == nil {
configs = strings.Replace(configs, "EDITOR_PATH", editor, -1)
}
// output the templated configs
fmt.Println(configs)

View File

@@ -64,7 +64,8 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
}
// process cheatpaths
for i, cheatpath := range conf.Cheatpaths {
var validPaths []cp.Cheatpath
for _, cheatpath := range conf.Cheatpaths {
// expand ~ in config paths
expanded, err := homedir.Expand(cheatpath.Path)
@@ -83,6 +84,14 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
if resolve {
evaled, err := filepath.EvalSymlinks(expanded)
if err != nil {
// if the path simply doesn't exist, warn and skip it
if os.IsNotExist(err) {
fmt.Fprintf(os.Stderr,
"WARNING: cheatpath '%s' does not exist, skipping\n",
expanded,
)
continue
}
return Config{}, fmt.Errorf(
"failed to resolve symlink: %s: %v",
expanded,
@@ -93,8 +102,10 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
expanded = evaled
}
conf.Cheatpaths[i].Path = expanded
cheatpath.Path = expanded
validPaths = append(validPaths, cheatpath)
}
conf.Cheatpaths = validPaths
// trim editor whitespace
conf.Editor = strings.TrimSpace(conf.Editor)

View File

@@ -189,10 +189,14 @@ cheatpaths:
t.Fatalf("failed to write config: %v", err)
}
// Load config with symlink resolution should fail
_, err = New(map[string]interface{}{}, configFile, true)
if err == nil {
t.Error("expected error for broken symlink, got nil")
// Load config with symlink resolution should skip the broken cheatpath
// (warn to stderr) rather than hard-error
conf, err := New(map[string]interface{}{}, configFile, true)
if err != nil {
t.Errorf("expected no error for broken symlink (should skip), got: %v", err)
}
if len(conf.Cheatpaths) != 0 {
t.Errorf("expected broken cheatpath to be filtered out, got %d cheatpaths", len(conf.Cheatpaths))
}
}

View File

@@ -17,13 +17,15 @@ func Run(configs string, confpath string) error {
// cheatsheets based on the user's platform
confdir := filepath.Dir(confpath)
// create paths for community and personal cheatsheets
// create paths for community, personal, and work cheatsheets
community := filepath.Join(confdir, "cheatsheets", "community")
personal := filepath.Join(confdir, "cheatsheets", "personal")
work := filepath.Join(confdir, "cheatsheets", "work")
// set default cheatpaths
configs = strings.Replace(configs, "COMMUNITY_PATH", community, -1)
configs = strings.Replace(configs, "PERSONAL_PATH", personal, -1)
configs = strings.Replace(configs, "WORK_PATH", work, -1)
// locate and set a default pager
configs = strings.Replace(configs, "PAGER_PATH", config.Pager(), -1)
@@ -44,15 +46,29 @@ func Run(configs string, confpath string) error {
// clone the community cheatsheets if so instructed
if yes {
// clone the community cheatsheets
fmt.Printf("Cloning community cheatsheets to %s.\n", community)
if err := repo.Clone(community); err != nil {
return fmt.Errorf("failed to clone cheatsheets: %v", err)
}
} else {
// comment out the community cheatpath in the config since
// the directory won't exist
configs = strings.Replace(configs,
" - name: community\n"+
" path: "+community+"\n"+
" tags: [ community ]\n"+
" readonly: true",
" #- name: community\n"+
" # path: "+community+"\n"+
" # tags: [ community ]\n"+
" # readonly: true",
-1,
)
}
// also create a directory for personal cheatsheets
fmt.Printf("Cloning personal cheatsheets to %s.\n", personal)
if err := os.MkdirAll(personal, os.ModePerm); err != nil {
// always create personal and work directories
for _, dir := range []string{personal, work} {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
}

View File

@@ -54,8 +54,8 @@ cheatpaths:
confpath: filepath.Join(tempDir, "conf1", "conf.yml"),
userInput: "n\n",
wantErr: false,
checkFiles: []string{"conf1/conf.yml"},
dontWantFiles: []string{"conf1/cheatsheets/community", "conf1/cheatsheets/personal"},
checkFiles: []string{"conf1/conf.yml", "conf1/cheatsheets/personal", "conf1/cheatsheets/work"},
dontWantFiles: []string{"conf1/cheatsheets/community"},
},
{
name: "user accepts but clone fails",
@@ -177,10 +177,18 @@ func TestRunStringReplacements(t *testing.T) {
editor: EDITOR_PATH
pager: PAGER_PATH
cheatpaths:
- name: community
path: COMMUNITY_PATH
- name: personal
path: PERSONAL_PATH
tags: [ personal ]
readonly: false
- name: work
path: WORK_PATH
tags: [ work ]
readonly: false
- name: community
path: COMMUNITY_PATH
tags: [ community ]
readonly: true
`
// Create temp directory
@@ -194,7 +202,6 @@ cheatpaths:
confdir := filepath.Dir(confpath)
// Expected paths
expectedCommunity := filepath.Join(confdir, "cheatsheets", "community")
expectedPersonal := filepath.Join(confdir, "cheatsheets", "personal")
// Save original stdin/stdout
@@ -244,10 +251,16 @@ cheatpaths:
if strings.Contains(contentStr, "PAGER_PATH") && !strings.Contains(contentStr, fmt.Sprintf("pager: %s", "")) {
t.Error("PAGER_PATH was not replaced")
}
if strings.Contains(contentStr, "WORK_PATH") {
t.Error("WORK_PATH was not replaced")
}
// Verify correct paths were used
if !strings.Contains(contentStr, expectedCommunity) {
t.Errorf("expected community path %q in config", expectedCommunity)
// Verify community path is commented out (user declined)
if strings.Contains(contentStr, " - name: community") {
t.Error("expected community cheatpath to be commented out when declined")
}
if !strings.Contains(contentStr, " #- name: community") {
t.Error("expected commented-out community cheatpath")
}
if !strings.Contains(contentStr, expectedPersonal) {
t.Errorf("expected personal path %q in config", expectedPersonal)