Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Release

# Cut a release when a version tag is pushed, e.g.:
# git tag v0.1.0 && git push origin v0.1.0
#
# GoReleaser (config in .goreleaser.yaml) cross-compiles static binaries for
# linux/macos/windows (amd64 + arm64) and publishes a GitHub Release with
# archives and checksums. The version is injected into `commit-sprout --version`
# from the tag.
on:
push:
tags:
- "v*"

permissions:
# Needed for GoReleaser to create the GitHub Release and upload assets.
contents: write

jobs:
goreleaser:
name: goreleaser
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history + tags so the changelog and version resolve correctly.
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.23"
check-latest: true

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
# Pin a concrete v2 line so releases are reproducible and match the
# config syntax validated in CI/dev.
version: "v2.4.4"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76 changes: 76 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# GoReleaser configuration for commit-sprout.
#
# On a pushed tag (vX.Y.Z) the release workflow (.github/workflows/release.yml)
# runs `goreleaser release`, which cross-compiles a static single binary for
# Linux, macOS, and Windows and publishes a GitHub Release with archives and
# checksums. The build is CGO-free so every artifact is a self-contained binary
# with no runtime to install -- exactly what a prompt-hot-path tool wants.
#
# Validate locally without publishing:
# goreleaser check
# goreleaser release --snapshot --clean
version: 2

project_name: commit-sprout

before:
hooks:
- go mod tidy

builds:
- id: commit-sprout
main: .
binary: commit-sprout
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
# Trim paths and inject the version/commit/date into cmd.version so
# `commit-sprout --version` reports the released tag.
flags:
- -trimpath
ldflags:
- -s -w
- -X github.com/rwrife/commit-sprout/cmd.version={{ .Version }}

archives:
- id: commit-sprout
# Keep names predictable: commit-sprout_<version>_<os>_<arch>.<ext>
name_template: >-
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
format: tar.gz
# Windows users generally expect a zip.
format_overrides:
- goos: windows
format: zip
files:
- README.md
- LICENSE

checksum:
name_template: "checksums.txt"

snapshot:
version_template: "{{ incpatch .Version }}-next"

changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- "^ci:"
- "Merge pull request"
- "Merge branch"

release:
draft: false
prerelease: auto
name_template: "commit-sprout {{ .Tag }}"
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 rwrife

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
90 changes: 84 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
`commit-sprout` reads your git history and turns "did I actually ship anything?" into something
you can *see*. It's local-first, single-binary, and has opinions about your commit cadence.

> Status: **early days** — see [PLAN.md](./PLAN.md) and the milestone issues.
> Status: **v0.1** — the plant renders, reacts to your commits, and drops into
> your prompt. See [PLAN.md](./PLAN.md) and the milestone issues for what's next.

## Why

Expand All @@ -29,13 +30,48 @@ dopamine as a contribution streak, but with a face (well, leaves).
```bash
commit-sprout # render the current plant for the repo you're in
commit-sprout status # streak, last commit, days until it wilts
commit-sprout --prompt # compact glyph for your shell prompt / tmux status
commit-sprout prompt # compact glyph for your shell prompt / tmux status
commit-sprout --prompt # same thing, as a flag (handy inside a prompt string)
commit-sprout water # (planned) buy a grace day before wilting
```

`status` prints a plain, script-friendly summary:

```text
stage: leafy (healthy)
streak: 6 days (best: 11 days)
last: today (2026-07-04)
health: healthy — 2 days of cushion before it gets thirsty
```

`prompt` prints a single glyph + stage (e.g. `ل leafy`). It is built for the
hot path: **read-only by default** (a prompt renders on every command, so it
skips the state write unless you pass `--save`) and it **fails silently outside
a git repo** — it prints nothing and exits `0`, so it can never break your
shell. Drop it in and forget it.

## Install

Not released as a prebuilt binary yet, but it already runs. With Go 1.23+ installed:
### Prebuilt binary (recommended)

Grab a static, single-file binary for your OS/arch from the
[latest release](https://github.com/rwrife/commit-sprout/releases/latest) —
builds are published for Linux, macOS, and Windows (amd64 + arm64), with
checksums. No runtime to install. Unpack it and drop `commit-sprout` on your
`PATH`, e.g.:

```bash
# Linux/macOS example (adjust the asset name to your OS/arch)
curl -sSL -o commit-sprout.tar.gz \
https://github.com/rwrife/commit-sprout/releases/latest/download/commit-sprout_<version>_linux_amd64.tar.gz
tar -xzf commit-sprout.tar.gz commit-sprout
install -m 0755 commit-sprout ~/.local/bin/ # or anywhere on your PATH
commit-sprout --version
```

### From source

With Go 1.23+ installed:

```bash
# Run straight from a clone
Expand All @@ -49,8 +85,50 @@ commit-sprout # same thing
commit-sprout --version
```

Once M6 lands there'll be a single static binary for Windows/macOS/Linux and a
tagged release. Watch the repo.
## Put it in your shell prompt

`commit-sprout prompt` emits one line (`glyph stage`) and is safe to call on
every prompt render — it does no disk writes by default and stays silent outside
a git repo. A few ways to wire it up:

**starship** (`~/.config/starship.toml`) — add a custom module:

```toml
[custom.commit_sprout]
command = "commit-sprout prompt"
when = "git rev-parse --is-inside-work-tree"
shell = ["sh", "--norc"]
format = "[$output]($style) "
style = "green"
```

**tmux** (`~/.tmux.conf`) — show it in the status line (polled, so keep the
interval sane):

```tmux
set -g status-interval 60
set -g status-right "#(cd #{pane_current_path} && commit-sprout prompt) | %H:%M"
```

**bash** (`~/.bashrc`) — prepend the glyph to your prompt:

```bash
__commit_sprout() { commit-sprout prompt 2>/dev/null; }
PROMPT_COMMAND='PS1_SPROUT=$(__commit_sprout)'
PS1='${PS1_SPROUT:+$PS1_SPROUT }\u@\h:\w\$ '
```

**zsh** (`~/.zshrc`) — same idea with a precmd hook:

```zsh
autoload -Uz add-zsh-hook
__commit_sprout() { psvar[1]="$(commit-sprout prompt 2>/dev/null)"; }
add-zsh-hook precmd __commit_sprout
setopt prompt_subst
PROMPT='%(1V.%1v .)%n@%m:%~%# '
```

All of these degrade cleanly: no repo, no glyph, no error.

## How it works (planned)

Expand Down Expand Up @@ -92,7 +170,7 @@ See [PLAN.md](./PLAN.md) for the full plan, milestones (M1–M6), and the v0.2+

## License

MIT (see [LICENSE](./LICENSE) once added).
MIT — see [LICENSE](./LICENSE).

---

Expand Down
79 changes: 79 additions & 0 deletions cmd/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Prompt subcommand: emit a single compact line -- a glyph plus the stage name
// -- suitable for embedding in a shell prompt (bash/zsh $PROMPT), a tmux status
// line, or a starship custom module.
//
// Design constraints (M6): prompt mode must be fast and must never block or
// break the shell. Two things follow from that:
//
// - It is read-only by default. A prompt renders on every command you run, so
// writing state each time would be needless disk churn; prompt mode skips
// the save unless you explicitly pass --save. (The root command and status
// still persist normally.)
// - It fails soft. Outside a git repo -- or if git errors for any reason --
// it prints nothing and exits 0, so a misconfigured prompt never dumps an
// error into your shell. Embed it and forget it.
package cmd

import (
"errors"
"fmt"

"github.com/rwrife/commit-sprout/internal/gitstat"
"github.com/rwrife/commit-sprout/internal/render"
"github.com/spf13/cobra"
)

// promptSave opts prompt mode back into persisting state. Off by default so the
// hot path (a prompt on every command) does no disk writes.
var promptSave bool

// promptCmd implements `commit-sprout prompt`.
var promptCmd = &cobra.Command{
Use: "prompt",
Short: "Emit a one-line glyph for a shell prompt, tmux, or starship",
Long: `Print a single compact line -- a glyph plus the plant's stage -- meant to
be embedded in a shell prompt, a tmux status line, or a starship custom module.

Prompt mode is built for the hot path: it is read-only by default (no state is
written, since a prompt renders constantly) and fails silently outside a git
repo, printing nothing and exiting 0 so it can never break your shell. Pass
--save if you do want a prompt render to update the plant's memory.`,
Args: cobra.NoArgs,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return renderPrompt(cmd)
},
}

// renderPrompt prints the compact glyph line. It swallows the not-a-repo case
// (and, defensively, does not surface git errors into the prompt) so that a
// prompt integration is always safe to leave in place.
func renderPrompt(cmd *cobra.Command) error {
r, err := runPipeline()
if err != nil {
if errors.Is(err, gitstat.ErrNotARepo) {
// Silent: nothing growing here, so contribute nothing to the prompt.
return nil
}
// Any other git failure also stays silent in prompt mode; a broken
// prompt is worse than a missing glyph.
return nil
}

if _, perr := fmt.Fprintln(cmd.OutOrStdout(), render.PromptGlyph(r.plant)); perr != nil {
return perr
}

// Prompt mode only persists when explicitly asked (--save). The --no-save
// persistent flag still wins if set.
if promptSave {
persist(r)
}
return nil
}

func init() {
// --save re-enables persistence for prompt mode (off by default).
promptCmd.Flags().BoolVar(&promptSave, "save", false, "persist plant state on this prompt render (off by default)")
}
Loading
Loading