From 4d1e6c8d9afc2f0cf3d54bd243f6b1f2e972fc8b Mon Sep 17 00:00:00 2001
From: zhravan
Date: Wed, 17 Sep 2025 04:29:02 +0530
Subject: [PATCH 1/3] docs: OSS starter docs
---
.github/ISSUE_TEMPLATE/bug_report.md | 32 +++++++++
.github/ISSUE_TEMPLATE/feature_request.md | 23 +++++++
.github/PULL_REQUEST_TEMPLATE.md | 17 +++++
CODE_OF_CONDUCT.md | 27 ++++++++
CONTRIBUTING.md | 80 +++++++++++++++++++++++
README.md | 5 ++
SECURITY.md | 13 ++++
7 files changed, 197 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
create mode 100644 .github/PULL_REQUEST_TEMPLATE.md
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 CONTRIBUTING.md
create mode 100644 SECURITY.md
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..80c2435
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: "[Bug] "
+labels: bug
+assignees: ''
+---
+
+### Describe the bug
+
+A clear and concise description of what the bug is.
+
+### To Reproduce
+
+Steps to reproduce the behavior:
+
+1. Command(s) run
+2. Output
+
+### Expected behavior
+
+What you expected to happen.
+
+### Environment
+
+- OS:
+- Go version:
+- GoLearn version:
+
+### Additional context
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..cc6459a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,23 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[Feature] "
+labels: enhancement
+assignees: ''
+---
+
+### Problem
+
+What problem does this feature solve?
+
+### Proposal
+
+Describe the solution you'd like.
+
+### Alternatives
+
+Describe alternatives you've considered.
+
+### Additional context
+
+Add any other context or screenshots.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..fc89ebb
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,17 @@
+## Summary
+
+Describe the change and its motivation.
+
+## Checklist
+
+- [ ] Tests pass: `make verify` or `golearn verify `
+- [ ] Docs updated (README/CONTRIBUTING) if needed
+- [ ] No large new dependencies
+
+## Screenshots / Output (if CLI UX)
+
+Paste before/after where helpful.
+
+## Related issues
+
+Fixes #
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..23db016
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,27 @@
+# Code of Conduct
+
+This project follows the Contributor Covenant Code of Conduct.
+
+- For maintainers and contributors: please act with empathy and respect.
+- Harassment and discrimination are not tolerated.
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
+
+## Our Standards
+
+- Use welcoming and inclusive language
+- Be respectful of differing viewpoints and experiences
+- Gracefully accept constructive criticism
+- Focus on what is best for the community
+
+## Enforcement
+
+Report unacceptable behavior to the maintainers at: .
+
+Project maintainers are responsible for clarifying and enforcing standards of acceptable behavior and will take appropriate and fair corrective action.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8261aba
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,80 @@
+# Contributing to GoLearn
+
+Thanks for your interest in contributing! This project aims to be a friendly, welcoming place to learn and teach Go.
+
+## Ways to contribute
+
+- Report bugs and propose enhancements
+- Improve exercises, hints, and documentation
+- Add new exercises or solutions (see below)
+- Improve the CLI UX and accessibility
+
+## Development setup
+
+```bash
+# Clone your fork
+git clone https://github.com//golearn
+cd golearn
+
+# Build the CLI
+make build # or: go build ./cmd/golearn
+
+# Run locally
+./bin/golearn help
+
+# Initialize embedded exercises into a workspace
+mkdir -p /tmp/golearn-ws && cd /tmp/golearn-ws
+/path/to/bin/golearn init
+```
+
+## Running tests
+
+This repository embeds template tests. To run tests for an exercise from the CLI repo:
+
+```bash
+# From repo root
+make verify NAME=01_hello # or: go run ./cmd/golearn verify 01_hello
+```
+
+## Code style
+
+- Go 1.22+
+- Match existing formatting; run `gofmt` and `go vet`
+- Prefer clear, readable code; add concise comments for non-obvious logic
+- Avoid adding heavy deps; keep the binary small and portable
+
+## Git workflow
+
+1. Create a feature branch
+2. Make focused commits with descriptive messages
+3. Ensure `make build` and `make verify` pass
+4. Open a PR with a clear title and description (screenshots welcome)
+
+## Adding or updating exercises
+
+- Edit templates under `internal/exercises/templates/`
+- Keep tests self-contained in the exercise folder
+- Provide hints in `internal/exercises/catalog.yaml`
+- Do not add solution code into templates; see solutions below
+
+## Adding solutions
+
+- Place solutions in `internal/exercises/solutions/`
+- Only include implementation files; do not include tests
+- Validate locally:
+
+```bash
+# Validate embedded solution against embedded tests
+./bin/golearn verify --solution
+```
+
+## Docs and UX
+
+- Update `README.md` if you add a new command or flag
+- Keep output accessible; follow `internal/cli/theme` guidance
+
+## License
+
+- Code is MIT. Non-code lesson content and artwork are under CC BY 3.0 (see CONTENT_LICENSE).
+
+Please also read the Code of Conduct.
diff --git a/README.md b/README.md
index 158aa79..f50ec18 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,11 @@ golearn progress # Rich TUI with ASCII progress bar and checklist
golearn watch # Watches ./exercises and re-runs tests per edited exercise
```
+### Contributing
+- See [CONTRIBUTING.md](./CONTRIBUTING.md)
+- Please follow our [Code of Conduct](./CODE_OF_CONDUCT.md)
+- Security issues: see [SECURITY.md](./SECURITY.md)
+
Need commands?
```bash
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..c20b998
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+We take security seriously. Please report vulnerabilities responsibly.
+
+## Supported versions
+We generally support the latest minor release of the CLI.
+
+## Reporting a vulnerability
+- Email: security@example.com
+- Please include steps to reproduce and any PoC code if available
+- Do not open public issues for security problems
+
+We will acknowledge your report within 72 hours and keep you updated.
From 0cd15bf3b922e259c386202135b2eaff3fbd74d0 Mon Sep 17 00:00:00 2001
From: zhravan
Date: Wed, 17 Sep 2025 05:06:07 +0530
Subject: [PATCH 2/3] feat(gamification): leaderboard gamification for fun
---
README.md | 27 ++-
internal/cli/cli.go | 10 ++
internal/cli/commands.go | 360 +++++++++++++++++++++++++++++++++++++++
3 files changed, 394 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index f50ec18..fce0104 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,22 @@
Rustlings‑style Go exercises in a tiny CLI.
+
+

+
+
### Why this exists
This project is my attempt to learn Go by building as I learn, making the journey more engaging while exploring the language in practice. I'm sharing the exercises and tooling so others can learn alongside me.
+### At a glance
+
+- Simple CLI to list and verify exercises
+- Helpful hints and solution links when you're stuck
+- Watch mode to auto-run tests on changes
+- Progress dashboard with a visual bar and checklists
+- Publish your progress to GitHub and appear on the README leaderboard
+
### Install (Go 1.22+)
```bash
@@ -35,9 +47,14 @@ golearn progress # Rich TUI with ASCII progress bar and checklist
# Auto-verify on change (watch mode)
golearn watch # Watches ./exercises and re-runs tests per edited exercise
+
+# Publish your progress (appears on README leaderboard)
+golearn publish --dry-run
+golearn publish --user
```
### Contributing
+
- See [CONTRIBUTING.md](./CONTRIBUTING.md)
- Please follow our [Code of Conduct](./CODE_OF_CONDUCT.md)
- Security issues: see [SECURITY.md](./SECURITY.md)
@@ -72,6 +89,10 @@ make watch
- Non-code lesson content and any included gopher artwork: [CC BY 3.0](./CONTENT_LICENSE).
- Inspired by rustlings and Go by Example. [NOTICE](./NOTICE) for attributions.
-
-

-
+## Leaderboard
+
+The following users have completed all exercises (ascending by completion time):
+
+
+No completions yet. Be the first!
+
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 8238351..05500f5 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -19,6 +19,7 @@ Usage:
golearn solution [name] Show solution flow (hint-first; or link)
golearn watch Watch files and re-run tests on change
golearn progress Show progress
+ golearn publish [options] Publish your progress to upstream as a PR
golearn reset [name] Reset exercise to starter state
golearn init [repo] [dir] Initialize workspace: clone exercises repo or copy built-in templates
golearn help Show this help
@@ -27,6 +28,12 @@ Global options:
--no-color Disable ANSI colors (honors NO_COLOR)
--theme= Theme: default | high-contrast | monochrome
--screen-reader, --sr Optimize for screen readers; avoid screen clears
+
+Publish options (env overrides in parentheses):
+ --repo= Upstream repo to contribute to (GOLEARN_PUBLISH_REPO)
+ --user= Your GitHub username (GOLEARN_PUBLISH_USER)
+ --branch= Branch to create for the PR
+ --dry-run Print JSON snapshot instead of creating a PR
`
}
@@ -72,6 +79,9 @@ func Execute(args []string) error {
return runWatch()
case "progress":
return runProgress()
+ case "publish":
+ // pass through remaining args to the publish handler
+ return runPublish(args[1:])
case "reset":
var name string
if len(args) > 1 {
diff --git a/internal/cli/commands.go b/internal/cli/commands.go
index 2b4d40b..3ee29fc 100644
--- a/internal/cli/commands.go
+++ b/internal/cli/commands.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io/ioutil"
"os"
"os/exec"
"os/signal"
@@ -463,3 +464,362 @@ func progressBarWidth() int {
}
return 60
}
+
+// runPublish collects local progress and attempts to create a PR against the upstream repo
+// adding/refreshing a JSON snapshot under contrib/leaderboard/progress/.json.
+// It prefers the GitHub CLI if available and the user is authenticated. Otherwise, it
+// prints the snapshot and manual steps.
+func runPublish(args []string) error {
+ repoURL, userName, branchName, dryRun := parsePublishFlags(args)
+ if repoURL == "" {
+ repoURL = strings.TrimSpace(os.Getenv("GOLEARN_PUBLISH_REPO"))
+ if repoURL == "" {
+ repoURL = "https://github.com/zhravan/golearn"
+ }
+ }
+ if userName == "" {
+ userName = strings.TrimSpace(os.Getenv("GOLEARN_PUBLISH_USER"))
+ }
+ if branchName == "" {
+ branchName = fmt.Sprintf("progress/%s-%s", sanitizeForFile(userName), time.Now().Format("20060102-150405"))
+ }
+
+ // Build snapshot
+ snap, err := buildProgressSnapshot(userName)
+ if err != nil {
+ return err
+ }
+
+ if dryRun {
+ b, _ := json.MarshalIndent(snap, "", " ")
+ fmt.Println(string(b))
+ fmt.Println(theme.Muted("Dry-run: not creating a PR."))
+ return nil
+ }
+
+ // Prefer GitHub CLI if present
+ if _, lookErr := exec.LookPath("gh"); lookErr != nil {
+ fmt.Println(theme.Muted("GitHub CLI not found. Printing snapshot and manual steps."))
+ return printManualPublishInstructions(repoURL, snap)
+ }
+
+ // Ensure auth
+ if err := exec.Command("gh", "auth", "status").Run(); err != nil {
+ fmt.Println(theme.Muted("GitHub CLI not authenticated. Run 'gh auth login' first."))
+ return printManualPublishInstructions(repoURL, snap)
+ }
+
+ // If username is still empty, derive from gh
+ if strings.TrimSpace(userName) == "" {
+ out, err := exec.Command("gh", "api", "user", "-q", ".login").Output()
+ if err == nil {
+ userName = strings.TrimSpace(string(out))
+ }
+ }
+
+ owner, name := parseRepoOwnerAndName(repoURL)
+ if owner == "" || name == "" {
+ return fmt.Errorf("unable to parse repo URL: %s", repoURL)
+ }
+
+ // Ensure fork exists (no-op if already exists)
+ _ = exec.Command("gh", "repo", "fork", fmt.Sprintf("%s/%s", owner, name), "--clone=false", "--remote=false").Run()
+
+ // Work in a temporary clone of upstream
+ tmpDir, err := ioutil.TempDir("", "golearn-publish-")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(tmpDir)
+
+ cloneCmd := exec.Command("git", "clone", repoURL, ".")
+ cloneCmd.Dir = tmpDir
+ cloneCmd.Stdout = os.Stdout
+ cloneCmd.Stderr = os.Stderr
+ if err := cloneCmd.Run(); err != nil {
+ return fmt.Errorf("git clone failed: %w", err)
+ }
+
+ // Create branch
+ if err := runCmd(tmpDir, "git", "checkout", "-b", branchName); err != nil {
+ return err
+ }
+
+ // Write snapshot file
+ progressDir := filepath.Join(tmpDir, "contrib", "leaderboard", "progress")
+ if err := os.MkdirAll(progressDir, 0o755); err != nil {
+ return err
+ }
+ fileName := sanitizeForFile(userName)
+ if fileName == "" {
+ fileName = "anonymous"
+ }
+ filePath := filepath.Join(progressDir, fileName+".json")
+ b, _ := json.MarshalIndent(snap, "", " ")
+ if err := os.WriteFile(filePath, b, 0o644); err != nil {
+ return err
+ }
+
+ // Update README leaderboard section
+ if err := updateLeaderboardReadme(tmpDir, progressDir); err != nil {
+ fmt.Printf("Warning: could not update README leaderboard: %v\n", err)
+ }
+
+ if err := runCmd(tmpDir, "git", "add", filePath); err != nil {
+ return err
+ }
+ _ = runCmd(tmpDir, "git", "add", filepath.Join(tmpDir, "README.md"))
+ msg := fmt.Sprintf("chore(leaderboard): add progress for %s (%d/%d)", snap.User, snap.CompletedCount, snap.TotalCount)
+ if err := runCmd(tmpDir, "git", "commit", "-m", msg); err != nil {
+ return err
+ }
+
+ // Add fork remote and push
+ forkURL := fmt.Sprintf("https://github.com/%s/%s.git", userName, name)
+ _ = runCmd(tmpDir, "git", "remote", "remove", "fork")
+ if err := runCmd(tmpDir, "git", "remote", "add", "fork", forkURL); err != nil {
+ return err
+ }
+ if err := runCmd(tmpDir, "git", "push", "-u", "fork", branchName+":"+branchName); err != nil {
+ return err
+ }
+
+ // Create PR against upstream
+ prTitle := fmt.Sprintf("Add progress for %s (%d/%d)", snap.User, snap.CompletedCount, snap.TotalCount)
+ prBody := "Automated progress publish from golearn CLI. This adds/updates your progress snapshot for the leaderboard."
+ prCmd := exec.Command("gh", "pr", "create",
+ "--repo", fmt.Sprintf("%s/%s", owner, name),
+ "--head", fmt.Sprintf("%s:%s", userName, branchName),
+ "--base", "main",
+ "--title", prTitle,
+ "--body", prBody,
+ )
+ prCmd.Dir = tmpDir
+ prCmd.Stdout = os.Stdout
+ prCmd.Stderr = os.Stderr
+ if err := prCmd.Run(); err != nil {
+ fmt.Println(theme.Muted("Could not auto-create PR. You may need to open it manually."))
+ fmt.Printf("Branch pushed to %s:%s\n", forkURL, branchName)
+ fmt.Printf("Open a PR against %s/%s with head %s:%s\n", owner, name, userName, branchName)
+ return nil
+ }
+
+ return nil
+}
+
+func parsePublishFlags(args []string) (repo, user, branch string, dry bool) {
+ for _, a := range args {
+ if strings.HasPrefix(a, "--repo=") {
+ repo = strings.TrimSpace(strings.TrimPrefix(a, "--repo="))
+ continue
+ }
+ if strings.HasPrefix(a, "--user=") {
+ user = strings.TrimSpace(strings.TrimPrefix(a, "--user="))
+ continue
+ }
+ if strings.HasPrefix(a, "--branch=") {
+ branch = strings.TrimSpace(strings.TrimPrefix(a, "--branch="))
+ continue
+ }
+ if a == "--dry-run" || a == "--dry" {
+ dry = true
+ continue
+ }
+ }
+ return
+}
+
+type progressSnapshot struct {
+ User string `json:"user"`
+ CompletedCount int `json:"completed_count"`
+ TotalCount int `json:"total_count"`
+ Percent int `json:"percent"`
+ CompletedSlugs []string `json:"completed_slugs"`
+ Timestamp string `json:"timestamp"`
+}
+
+func buildProgressSnapshot(user string) (progressSnapshot, error) {
+ catalog, err := exercises.ListAll()
+ if err != nil {
+ return progressSnapshot{}, err
+ }
+ var all []exercises.Exercise
+ all = append(all, catalog.Concepts...)
+ all = append(all, catalog.Projects...)
+ sort.Slice(all, func(i, j int) bool { return all[i].Slug < all[j].Slug })
+
+ var completed []string
+ for _, ex := range all {
+ ok, _ := progress.IsCompleted(ex.Slug)
+ if ok {
+ completed = append(completed, ex.Slug)
+ }
+ }
+ percent := 0
+ if len(all) > 0 {
+ percent = int(float64(len(completed)) / float64(len(all)) * 100)
+ }
+ return progressSnapshot{
+ User: user,
+ CompletedCount: len(completed),
+ TotalCount: len(all),
+ Percent: percent,
+ CompletedSlugs: completed,
+ Timestamp: time.Now().UTC().Format(time.RFC3339),
+ }, nil
+}
+
+func sanitizeForFile(s string) string {
+ s = strings.TrimSpace(strings.ToLower(s))
+ if s == "" {
+ return ""
+ }
+ // replace non-alphanumeric with '-'
+ var b strings.Builder
+ for _, r := range s {
+ if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
+ b.WriteRune(r)
+ } else {
+ b.WriteRune('-')
+ }
+ }
+ return strings.Trim(b.String(), "-")
+}
+
+func parseRepoOwnerAndName(repoURL string) (owner, name string) {
+ url := strings.TrimSpace(repoURL)
+ url = strings.TrimSuffix(url, ".git")
+ if strings.Contains(url, "github.com") {
+ // handle https and ssh
+ if strings.HasPrefix(url, "git@") {
+ // git@github.com:owner/name(.git)
+ parts := strings.SplitN(url, ":", 2)
+ if len(parts) == 2 {
+ rest := parts[1]
+ segs := strings.Split(strings.TrimPrefix(rest, "/"), "/")
+ if len(segs) >= 2 {
+ return segs[0], segs[1]
+ }
+ }
+ } else {
+ // https://github.com/owner/name
+ idx := strings.Index(url, "github.com/")
+ if idx >= 0 {
+ rest := url[idx+len("github.com/"):]
+ segs := strings.Split(strings.TrimPrefix(rest, "/"), "/")
+ if len(segs) >= 2 {
+ return segs[0], segs[1]
+ }
+ }
+ }
+ }
+ return "", ""
+}
+
+func runCmd(dir string, name string, args ...string) error {
+ cmd := exec.Command(name, args...)
+ cmd.Dir = dir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ return cmd.Run()
+}
+
+func printManualPublishInstructions(repoURL string, snap progressSnapshot) error {
+ b, _ := json.MarshalIndent(snap, "", " ")
+ fmt.Println(string(b))
+ fmt.Println()
+ fmt.Println(theme.Muted("Manual steps to publish your progress:"))
+ fmt.Println("1) Fork the repository if not already: https://github.com/zhravan/golearn")
+ fmt.Println("2) Clone upstream, create a branch, add the JSON under contrib/leaderboard/progress/.json, commit, push to your fork, and open a PR.")
+ fmt.Println(" Repo:", repoURL)
+ return nil
+}
+
+type leaderRow struct {
+ User string
+ Timestamp string
+}
+
+func updateLeaderboardReadme(repoDir string, progressDir string) error {
+ entries, err := os.ReadDir(progressDir)
+ if err != nil {
+ return err
+ }
+ var rows []leaderRow
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
+ continue
+ }
+ b, err := os.ReadFile(filepath.Join(progressDir, e.Name()))
+ if err != nil {
+ continue
+ }
+ var ps progressSnapshot
+ if err := json.Unmarshal(b, &ps); err != nil {
+ continue
+ }
+ if ps.TotalCount > 0 && ps.CompletedCount == ps.TotalCount {
+ rows = append(rows, leaderRow{User: ps.User, Timestamp: ps.Timestamp})
+ }
+ }
+ // sort by timestamp ascending, then username
+ sort.Slice(rows, func(i, j int) bool {
+ if rows[i].Timestamp == rows[j].Timestamp {
+ return rows[i].User < rows[j].User
+ }
+ return rows[i].Timestamp < rows[j].Timestamp
+ })
+
+ // Build dynamic content only within markers
+ var section strings.Builder
+ section.WriteString("\n")
+ if len(rows) == 0 {
+ section.WriteString("No completions yet. Be the first!\n")
+ } else {
+ for i, r := range rows {
+ section.WriteString(fmt.Sprintf("%d. %s (%s)\n", i+1, r.User, r.Timestamp))
+ }
+ }
+ section.WriteString("\n")
+
+ readmePath := filepath.Join(repoDir, "README.md")
+ old, err := os.ReadFile(readmePath)
+ if err != nil {
+ // If README missing, create a new README with header and section
+ var full strings.Builder
+ full.WriteString("## Leaderboard\n\n")
+ full.WriteString("The following users have completed all exercises (ascending by completion time):\n\n")
+ full.WriteString(section.String())
+ return os.WriteFile(readmePath, []byte(full.String()), 0o644)
+ }
+
+ // If markers are present, replace content between them; else append full section at end
+ updated := replaceBetweenMarkers(string(old), "", "", section.String())
+ if updated == string(old) {
+ // markers not found; append header + section
+ var full strings.Builder
+ full.WriteString(string(old))
+ if !strings.HasSuffix(string(old), "\n") {
+ full.WriteString("\n")
+ }
+ full.WriteString("\n## Leaderboard\n\n")
+ full.WriteString("The following users have completed all exercises (ascending by completion time):\n\n")
+ full.WriteString(section.String())
+ updated = full.String()
+ }
+ return os.WriteFile(readmePath, []byte(updated), 0o644)
+}
+
+func replaceBetweenMarkers(orig string, startMarker string, endMarker string, replacement string) string {
+ startIdx := strings.Index(orig, startMarker)
+ endIdx := strings.Index(orig, endMarker)
+ if startIdx == -1 || endIdx == -1 || endIdx < startIdx {
+ // append section at end
+ if strings.HasSuffix(orig, "\n") {
+ return orig + "\n" + replacement
+ }
+ return orig + "\n\n" + replacement
+ }
+ endIdx += len(endMarker)
+ return orig[:startIdx] + replacement + orig[endIdx:]
+}
From 121d473aaeee3468bdc6d888e421d20b59d243ca Mon Sep 17 00:00:00 2001
From: zhravan
Date: Wed, 17 Sep 2025 07:29:39 +0530
Subject: [PATCH 3/3] feat(gamification): tabular preview of the progress
---
internal/cli/commands.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/internal/cli/commands.go b/internal/cli/commands.go
index 3ee29fc..5fadb54 100644
--- a/internal/cli/commands.go
+++ b/internal/cli/commands.go
@@ -776,8 +776,12 @@ func updateLeaderboardReadme(repoDir string, progressDir string) error {
if len(rows) == 0 {
section.WriteString("No completions yet. Be the first!\n")
} else {
- for i, r := range rows {
- section.WriteString(fmt.Sprintf("%d. %s (%s)\n", i+1, r.User, r.Timestamp))
+ section.WriteString("| Image | Username | Date |\n")
+ section.WriteString("|---|---|---|\n")
+ for _, r := range rows {
+ avatar := fmt.Sprintf("https://github.com/%s.png?size=64", r.User)
+ profile := fmt.Sprintf("https://github.com/%s", r.User)
+ section.WriteString(fmt.Sprintf("|  | [%s](%s) | %s |\n", r.User, avatar, r.User, profile, r.Timestamp))
}
}
section.WriteString("\n")