From 6f919fd675717b18358c5612ddedc3d1d8caabbf Mon Sep 17 00:00:00 2001 From: Christopher Allen Lane Date: Sun, 15 Feb 2026 06:15:42 -0500 Subject: [PATCH] fix: comment out community cheatpath in --init output (#773) cheat --init now comments out the community cheatpath by default and includes a git clone instruction with the resolved path. This prevents warnings about missing directories when users save the --init output as their config without also cloning community cheatsheets. Closes #773 Co-Authored-By: Claude Opus 4.6 --- cmd/cheat/cmd_init.go | 14 +++ cmd/cheat/config.go | 3 +- cmd/cheat/first_run_integration_test.go | 140 ++++++++++++++++++++++-- 3 files changed, 148 insertions(+), 9 deletions(-) diff --git a/cmd/cheat/cmd_init.go b/cmd/cheat/cmd_init.go index e5fa5d2..4076d14 100644 --- a/cmd/cheat/cmd_init.go +++ b/cmd/cheat/cmd_init.go @@ -62,6 +62,20 @@ func cmdInit() { configs = strings.Replace(configs, "EDITOR_PATH", editor, -1) } + // comment out the community cheatpath by default, since the directory + // won't exist until the user clones it + 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, + ) + // output the templated configs fmt.Println(configs) } diff --git a/cmd/cheat/config.go b/cmd/cheat/config.go index db62ff1..899e45e 100644 --- a/cmd/cheat/config.go +++ b/cmd/cheat/config.go @@ -56,7 +56,8 @@ cheatpaths: tags: [ work ] readonly: false - # Community cheatsheets are stored here by default: + # Community cheatsheets (https://github.com/cheat/cheatsheets): + # To install: git clone https://github.com/cheat/cheatsheets COMMUNITY_PATH - name: community path: COMMUNITY_PATH tags: [ community ] diff --git a/cmd/cheat/first_run_integration_test.go b/cmd/cheat/first_run_integration_test.go index 6a699bb..e70b2dc 100644 --- a/cmd/cheat/first_run_integration_test.go +++ b/cmd/cheat/first_run_integration_test.go @@ -24,6 +24,59 @@ func TestFirstRunIntegration(t *testing.T) { t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output) } + t.Run("init comments out community", func(t *testing.T) { + testHome := t.TempDir() + env := firstRunEnv(testHome) + + cmd := exec.Command(binPath, "--init") + cmd.Env = env + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("--init failed: %v\nOutput: %s", err, output) + } + outStr := string(output) + + // No placeholder strings should survive (regression for #721) + assertNoPlaceholders(t, outStr) + + // Community cheatpath should be commented out + assertCommunityCommentedOut(t, outStr) + + // Personal and work cheatpaths should be active (uncommented) + assertCheatpathActive(t, outStr, "personal") + assertCheatpathActive(t, outStr, "work") + + // Should include clone instructions + if !strings.Contains(outStr, "git clone") { + t.Error("expected git clone instructions in --init output") + } + + // Save the config and verify it loads without errors. + // --init only outputs config, it doesn't create directories, + // so we need to create the cheatpath dirs the config references. + confpath := filepath.Join(testHome, "conf.yml") + if err := os.WriteFile(confpath, output, 0644); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + // Determine the confdir that --init used (same logic as cmd_init.go) + initConfpaths := firstRunConfpaths(testHome) + initConfdir := filepath.Dir(initConfpaths[0]) + for _, name := range []string{"personal", "work"} { + dir := filepath.Join(initConfdir, "cheatsheets", name) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("failed to create %s dir: %v", name, err) + } + } + + cmd2 := exec.Command(binPath, "--directories") + cmd2.Env = append(append([]string{}, env...), "CHEAT_CONFIG_PATH="+confpath) + output2, err := cmd2.CombinedOutput() + if err != nil { + t.Fatalf("config from --init failed to load: %v\nOutput: %s", err, output2) + } + }) + t.Run("decline config creation", func(t *testing.T) { testHome := t.TempDir() env := firstRunEnv(testHome) @@ -67,19 +120,22 @@ func TestFirstRunIntegration(t *testing.T) { t.Fatalf("config file not found at %s", confpath) } - // Verify community cheatpath is commented out in config + // Verify config file contents content, err := os.ReadFile(confpath) if err != nil { t.Fatalf("failed to read config: %v", err) } contentStr := string(content) - for _, line := range strings.Split(contentStr, "\n") { - trimmed := strings.TrimSpace(line) - if trimmed == "- name: community" { - t.Error("community cheatpath should be commented out") - break - } - } + + // No placeholder strings should survive (regression for #721) + assertNoPlaceholders(t, contentStr) + + // Community cheatpath should be commented out + assertCommunityCommentedOut(t, contentStr) + + // Personal and work cheatpaths should be active (uncommented) + assertCheatpathActive(t, contentStr, "personal") + assertCheatpathActive(t, contentStr, "work") // Verify personal and work directories were created confdir := filepath.Dir(confpath) @@ -163,6 +219,74 @@ func parseCreatedConfPath(t *testing.T, output string) string { return strings.TrimSpace(rest) } +// firstRunConfpaths returns the config file paths that cheat would check +// for the given home directory, matching the logic in config.Paths(). +func firstRunConfpaths(home string) []string { + switch runtime.GOOS { + case "windows": + return []string{ + filepath.Join(home, "AppData", "Roaming", "cheat", "conf.yml"), + } + default: + return []string{ + filepath.Join(home, ".config", "cheat", "conf.yml"), + } + } +} + +// assertNoPlaceholders verifies that no template placeholder strings survived +// in the config output. This is the regression check for #721 (literal +// PAGER_PATH appearing in the config). +func assertNoPlaceholders(t *testing.T, content string) { + t.Helper() + placeholders := []string{ + "PAGER_PATH", + "COMMUNITY_PATH", + "PERSONAL_PATH", + "WORK_PATH", + } + for _, p := range placeholders { + if strings.Contains(content, p) { + t.Errorf("placeholder %q was not replaced in config", p) + } + } + // EDITOR_PATH is special: it survives if no editor is found. + // In our test env EDITOR=vi is set, so it should be replaced. + if strings.Contains(content, "editor: EDITOR_PATH") { + t.Error("placeholder EDITOR_PATH was not replaced in config") + } +} + +// assertCommunityCommentedOut verifies that the community cheatpath entry +// is commented out (not active) in the config. +func assertCommunityCommentedOut(t *testing.T, content string) { + t.Helper() + for _, line := range strings.Split(content, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "- name: community" { + t.Error("community cheatpath should be commented out") + return + } + } + if !strings.Contains(content, "#- name: community") { + t.Error("expected commented-out community cheatpath") + } +} + +// assertCheatpathActive verifies that a named cheatpath is present and +// uncommented in the config. +func assertCheatpathActive(t *testing.T, content string, name string) { + t.Helper() + marker := "- name: " + name + for _, line := range strings.Split(content, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == marker { + return + } + } + t.Errorf("expected active (uncommented) cheatpath %q", name) +} + // firstRunConfigExists checks whether a cheat config file exists under the // given home directory at any of the standard locations. func firstRunConfigExists(home string) bool {