···11-# `storm`
11+# storm
2233-> A Go-based changelog manager built for clarity, speed, and interaction.
33+> Local-first changelog manager with TUIs for review and release.
4455-## Goals
66-77-- Use Git as a data source, not a dependency.
88-- Store unreleased notes locally (`.changes/*.md`) in a simple, editable format.
99-- Provide a terminal UI for reviewing commits and changes interactively.
1010-- Generate Markdown in strict [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
55+## Highlights
1161212-### Architecture
77+- **Keep a Changelog native:** unreleased notes live in `.changes/*.md` until you promote them.
88+- **Toolchain aware:** `storm bump`/`storm release` can update Cargo, npm, Python, and Deno manifests.
99+- **TUI friendly:** commit selectors, diff viewers, and toolchain pickers reuse the same palette and key bindings.
1010+- **Scriptable CLI:** every subcommand prints concise status messages suitable for CI logs.
13111414-- **Git integration:** Uses `go-git` for commit history and tag resolution — no shell calls.
1515-- **Diffing:** Custom lightweight diff engine for readable line-by-line output.
1616-- **Unreleased storage:** Simple Markdown files with YAML frontmatter (no external formats).
1717-- **Interactive mode:** Bubble Tea model for categorizing and confirming changes.
1818-- **Output:** Always produces Keep a Changelog–compliant Markdown.
1919-2020-## Core Packages
1212+## Install
21132214```sh
2323-.
2424-├── cmd
2525-├── internal
2626-│ ├── gitlog # Parse and structure commit history via `go-git`
2727-│ ├── diff # Minimal line diff for display and review
2828-│ ├── changeset # Manage `.changes/*.md` files
2929-│ ├── changelog # Build and update `CHANGELOG.md` sections
3030-│ ├── ui # Bubble Tea–based interactive interface
3131-│ └── style # Centralized Lip Gloss palette and formatting
3232-├── PROJECT.md
3333-└── README.md
1515+go install github.com/stormlightlabs/git-storm/cmd/storm@latest
3416```
35173636-## Command Model
1818+(Need Homebrew? Use the `storm.rb` formula template in this repo to build a tap.)
37193838-### Unreleased Changes
2020+## Quick Start
39214022```sh
4141-storm unreleased add --type added --scope cli --summary "Add changelog command"
4242-storm unreleased list
2323+storm generate --since v1.2.0 --interactive
4324storm unreleased review
2525+storm release --bump patch --toolchain package.json --tag
4426```
45274646-Adds or reviews pending `.changes/*.md` entries.
2828+## Documentation
47294848-### Generate From Git
3030+- [Introduction](docs/introduction.md)
3131+- [Quickstart](docs/quickstart.md)
3232+- [Manual](docs/manual.md)
3333+- [Development Guide](docs/development.md)
49345050-```sh
5151-storm generate <from> <to> [--interactive]
5252-```
3535+For a deeper dive into release automation, see `PROJECT.md`.
53365454-Pulls commits between refs, categorizes them by prefix, and optionally opens an interactive review.
3737+## Contributing
55385656-### Release
3939+Run the full test suite before opening a PR:
57405841```sh
5959-storm release --version 1.3.0 [--tag]
4242+go test ./...
6043```
61446262-Merges `.changes/*.md` into the changelog, writes a new section, and optionally tags the repository.
6363-6464-## Development Guidance
6565-6666-1. Composable
6767- Each subsystem (`diff`, `gitlog`, `tui`, etc.) should work standalone and be callable from tests or other Go programs.
6868-2. Frontmatter
6969-7070- ```yaml
7171- type: added
7272- scope: cli
7373- summary: Add changelog command
7474- ```
7575-7676-3. Consistent Palette
7777-7878- See package style for the color palette.
7979-8080-4. Commands should chain naturally and script cleanly:
8181-8282- ```sh
8383- storm unreleased list --json
8484- storm generate --since v1.2.0 --interactive
8585- storm release --version 1.3.0
8686- ```
8787-8888-5. Tests
8989- - Research testing bubbletea programs
9090- - Use golden files for diff/changelog output.
9191- - Use in-memory `go-git` repos in unit tests.
9292-9393-## Roadmap
9494-9595-| Phase | Deliverable |
9696-| ----- | ---------------------------------------------- |
9797-| 1 | Core CLI (`generate`, `unreleased`, `release`) |
9898-| 2 | Git integration and commit parsing |
9999-| 3 | Diff engine and styling |
100100-| 4 | `.changes` storage and parsing |
101101-| 5 | Interactive TUI |
102102-| 6 | Keep a Changelog writer |
103103-| 7 | Git tagging and CI integration |
104104-105105-## Notes
106106-107107-- No external dependencies beyond `cobra`, `go-git`, `bubbletea`, `lipgloss`, and `yaml.v3`.
108108-- Keep the workflow simple and reproducible so changelogs can be deterministically derived from local data.
109109-- Make sure interactive sessions degrade gracefully in non-TTY environments.
110110-111111-## Conventional Commits
112112-113113-### Structure
114114-115115-| Element | Format | Description |
116116-| ------------------------- | ---------------------------------------- | ---------------------------------------- |
117117-| Header | `<type>(<scope>): <description>` | The main commit message line. |
118118-| Scope | Optional, e.g. `api`, `cli`, `deps` | Indicates part of the codebase affected. |
119119-| Breaking Change Indicator | `!` after type/scope, e.g. `feat(api)!:` | Marks a breaking API change. |
120120-| Body | (Optional) one blank line then body text | Explanation of what & why. |
121121-| Footer | (Optional) one blank line then meta info | Issue refs, `BREAKING CHANGE: …`, etc |
122122-123123-### Types
124124-125125-| Type | Description |
126126-| ---------- | ----------------------------------------------------------------------- |
127127-| `feat` | A new feature. |
128128-| `fix` | A bug fix. |
129129-| `docs` | Documentation only changes. |
130130-| `style` | Code style changes (formatting, whitespace) that don’t affect behavior. |
131131-| `refactor` | Code changes that neither fix a bug nor add a feature. |
132132-| `perf` | Performance improvements. |
133133-| `test` | Adding or updating tests. |
134134-| `build` | Changes that affect the build system or dependencies. |
135135-| `ci` | Changes to CI configuration and scripts. |
136136-| `chore` | Other changes that don’t touch src/test (e.g., tooling, config). |
137137-| `revert` | Reverts a previous commit. |
138138-139139-### Examples
140140-141141-```text
142142-feat(api): add pagination endpoint
143143-144144-fix(ui): correct button alignment issue
145145-146146-docs: update README installation instructions
147147-148148-perf(core): optimize user query performance
149149-150150-refactor: restructure payment module for clarity
151151-152152-style: apply consistent formatting
153153-154154-test(auth): add integration tests for OAuth flow
155155-156156-build(deps): bump dependencies to latest versions
157157-158158-ci: add GitHub Actions workflow for CI
159159-160160-chore: update .gitignore and clean up obsolete files
161161-162162-feat(api)!: remove support for legacy endpoints
163163-164164-BREAKING CHANGE: API no longer accepts XML-formatted requests.
165165-```
166166-167167-### Reference
168168-169169-<https://www.conventionalcommits.org/en/v1.0.0/> "Conventional Commits"
4545+Issues and feature ideas are welcome—Storm is intentionally modular so new
4646+commands and TUIs can be added without touching the entire codebase.
···11----
22-outline: deep
33----
44-55-# Runtime API Examples
66-77-This page demonstrates usage of some of the runtime APIs provided by VitePress.
88-99-The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
1010-1111-```md
1212-<script setup>
1313-import { useData } from 'vitepress'
1414-1515-const { theme, page, frontmatter } = useData()
1616-</script>
1717-1818-## Results
1919-2020-### Theme Data
2121-<pre>{{ theme }}</pre>
2222-2323-### Page Data
2424-<pre>{{ page }}</pre>
2525-2626-### Page Frontmatter
2727-<pre>{{ frontmatter }}</pre>
2828-```
2929-3030-<script setup>
3131-import { useData } from 'vitepress'
3232-3333-const { site, theme, page, frontmatter } = useData()
3434-</script>
3535-3636-## Results
3737-3838-### Theme Data
3939-<pre>{{ theme }}</pre>
4040-4141-### Page Data
4242-<pre>{{ page }}</pre>
4343-4444-### Page Frontmatter
4545-<pre>{{ frontmatter }}</pre>
4646-4747-## More
4848-4949-Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
+105
docs/development.md
···11+---
22+title: Development
33+outline: deep
44+---
55+66+# Development
77+88+Storm is designed to be hackable: each package works on its own and can be
99+composed in tests or other Go programs. This document contains the guidance that
1010+previously lived in the repository README.
1111+1212+## Guidance
1313+1414+1. **Composable:** packages such as `diff`, `gitlog`, and `tui` should expose
1515+ standalone entry points that can be imported elsewhere.
1616+2. **Frontmatter:** `.changes/*.md` entries follow this schema:
1717+1818+ ```yaml
1919+ type: added
2020+ scope: cli
2121+ summary: Add changelog command
2222+ ```
2323+2424+3. **Palette:** all TUIs must use the colors defined in `internal/style`.
2525+4. **Command chaining:** every command should behave well in pipelines, e.g.
2626+2727+ ```sh
2828+ storm unreleased list --json
2929+ storm generate --since v1.2.0 --interactive
3030+ storm release --bump patch --toolchain package.json
3131+ ```
3232+3333+5. **Tests:**
3434+ - Prefer teatest for Bubble Tea programs.
3535+ - Use golden files for diff/changelog output when useful.
3636+ - Spin up in-memory `go-git` repositories in unit tests.
3737+3838+## Notes
3939+4040+- Keep the workflow deterministic so releases can be derived from local files
4141+ alone.
4242+- TUIs should degrade gracefully when `stdin`/`stdout` are not TTYs.
4343+- The binary should not depend on external services beyond git data already in
4444+ the repo.
4545+4646+## Roadmap
4747+4848+| Phase | Deliverable |
4949+| ----- | ---------------------------------------------- |
5050+| 1 | Core CLI (`generate`, `unreleased`, `release`) |
5151+| 2 | Git integration and commit parsing |
5252+| 3 | Diff engine and styling |
5353+| 4 | `.changes` storage and parsing |
5454+| 5 | Interactive TUI |
5555+| 6 | Keep a Changelog writer |
5656+| 7 | Git tagging and CI integration |
5757+5858+## Conventional Commits
5959+6060+Storm follows the [Conventional Commits](https://www.conventionalcommits.org)
6161+spec. Use the format `type(scope): summary` with optional body and footers.
6262+6363+### Structure
6464+6565+| Element | Format | Description |
6666+| ------- | ------ | ----------- |
6767+| Header | `<type>(<scope>): <description>` | Main commit line. |
6868+| Scope | Optional, e.g. `api`, `cli`, `deps`. | Part of the codebase affected. |
6969+| Breaking indicator | `!` after type/scope, e.g. `feat(api)!:` | Marks breaking change. |
7070+| Body | Blank line then body text. | Explains what and why. |
7171+| Footer | Blank line then metadata. | Issue references, `BREAKING CHANGE`, etc. |
7272+7373+### Types
7474+7575+| Type | Description |
7676+| ---- | ----------- |
7777+| `feat` | New feature. |
7878+| `fix` | Bug fix. |
7979+| `docs` | Documentation change. |
8080+| `style` | Formatting-only change. |
8181+| `refactor` | Structural change without new features or fixes. |
8282+| `perf` | Performance improvement. |
8383+| `test` | Adds or updates tests. |
8484+| `build` | Build system or dependency change. |
8585+| `ci` | CI config change. |
8686+| `chore` | Tooling or config change outside src/test. |
8787+| `revert` | Reverts a previous commit. |
8888+8989+### Examples
9090+9191+```text
9292+feat(api): add pagination endpoint
9393+fix(ui): correct button alignment issue
9494+docs: update README installation instructions
9595+perf(core): optimize user query performance
9696+refactor: restructure payment module for clarity
9797+style: apply consistent formatting
9898+test(auth): add integration tests for OAuth flow
9999+build(deps): bump dependencies to latest versions
100100+ci: add GitHub Actions workflow for CI
101101+chore: update .gitignore and clean up obsolete files
102102+feat(api)!: remove support for legacy endpoints
103103+104104+BREAKING CHANGE: API no longer accepts XML-formatted requests.
105105+```
+11-18
docs/index.md
···11---
22-# https://vitepress.dev/reference/default-theme-home-page
32layout: home
44-53hero:
66- name: "Git Storm"
77- text: "A changelog manager"
88- tagline: My great project tagline
44+ name: "Storm"
55+ text: "Local-first changelog manager"
66+ tagline: "Collect unreleased notes, review them in TUIs, and publish semantic releases without leaving git"
97 actions:
108 - theme: brand
1111- text: Markdown Examples
1212- link: /markdown-examples
1313- - theme: alt
1414- text: API Examples
1515- link: /api-examples
1616-99+ text: Quickstart
1010+ link: /quickstart
1711features:
1818- - title: Feature A
1919- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
2020- - title: Feature B
2121- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
2222- - title: Feature C
2323- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
1212+ - title: Keep a Changelog native
1313+ details: Entries stay in `.changes/*.md` until you promote them, keeping releases reproducible and reviewable.
1414+ - title: Toolchain aware
1515+ details: The bump and release commands can update Cargo, npm, Python, and Deno manifests in lockstep.
1616+ - title: Built for TUIs
1717+ details: Commit selectors, unreleased reviews, and diff viewers are powered by Bubble Tea and share the same palette.
2418---
2525-
+93
docs/introduction.md
···11+---
22+title: Introduction
33+outline: deep
44+---
55+66+# Introduction
77+88+Storm is a CLI that keeps changelog entries close to your code. It grew from a
99+few principles:
1010+1111+1. **Plain text first.** Every unreleased change is a Markdown file that lives
1212+ in your repo, making reviews and rebases simple.
1313+2. **Deterministic releases.** Given the same `.changes` directory, Storm will
1414+ always write the same `CHANGELOG.md` section.
1515+3. **Interactive when helpful, scriptable everywhere.** TUIs exist for reviews
1616+ and diffs, but every action prints machine-readable summaries for CI.
1717+1818+## Why Storm?
1919+2020+Storm sits between `git log` and `CHANGELOG.md`. It understands conventional
2121+commits, keeps notes in version control, and prefers deterministic text files
2222+over generated blobs. The CLI is written in Go, so it ships as a single binary
2323+that runs anywhere your repository does.
2424+2525+- **Local-first workflow:** no external services or databases.
2626+- **Deterministic releases:** `storm release` is idempotent and can run in CI.
2727+- **Composable commands:** each subcommand prints useful summaries for scripts.
2828+2929+## Quick Preview
3030+3131+```sh
3232+# Extract commits into .changes entries
3333+storm generate --since v1.2.0 --interactive
3434+3535+# Review pending notes
3636+storm unreleased review
3737+3838+# Cut a new release and update package.json
3939+storm release --bump minor --toolchain package.json --tag
4040+```
4141+4242+Need the details? Head to the [Quickstart](/quickstart) for a guided flow or
4343+read the [manual](/manual) for every flag and exit code.
4444+4545+## Architecture Overview
4646+4747+```sh
4848+.git/
4949+.changes/
5050+CHANGELOG.md
5151+```
5252+5353+- `storm generate` and `storm unreleased add` populate `.changes/`.
5454+- `storm check` and your CI ensure nothing merges without an entry.
5555+- `storm release` merges the queue into `CHANGELOG.md`, optionally creates a
5656+tag, and can update external manifests.
5757+5858+## Toolchain-aware versioning
5959+6060+The bump and release commands understand common ecosystem manifests:
6161+6262+| Manifest | Alias | Notes |
6363+| -------- | ----- | ----- |
6464+| `Cargo.toml` | `cargo`, `rust` | Updates `[package]` version. |
6565+| `pyproject.toml` | `pyproject`, `python`, `poetry` | Supports `[project]` and `[tool.poetry]`. |
6666+| `package.json` | `npm`, `node`, `package` | Edits the top-level `version` field. |
6767+| `deno.json` | `deno` | Updates the root `version`. |
6868+6969+Pass specific paths or the literal `interactive` to launch the toolchain picker
7070+TUI.
7171+7272+## TUIs everywhere
7373+7474+Storm shares a consistent palette (`internal/style`) across Bubble Tea
7575+experiences:
7676+7777+- **Commit selector** for `storm generate --interactive`.
7878+- **Unreleased review** for curating `.changes` entries.
7979+- **Diff viewer** for `storm diff` with split/unified modes.
8080+- **Toolchain picker** accessible via `--toolchain interactive`.
8181+8282+Each interface supports familiar Vim-style navigation (↑/↓, g/G, space to
8383+select, `q` to quit) and degrades gracefully when no TTY is available.
8484+8585+## Suggested Workflow
8686+8787+1. Developers add `.changes` entries alongside feature branches.
8888+2. Pull requests run `storm check --since <last-release>`.
8989+3. Release engineers run `storm generate` (if needed) then `storm release`.
9090+4. CI tags the release and publishes artifacts.
9191+9292+Need concrete steps? See the [Quickstart](/quickstart) or jump to the
9393+[manual](/manual).
+171
docs/manual.md
···11+---
22+title: Storm CLI Manual
33+---
44+55+# NAME
66+77+**storm** is a git powered aware changelog manager for Go projects.
88+99+## SYNOPSIS
1010+1111+```text
1212+storm [--repo <path>] [--output <file>] <command> [flags]
1313+```
1414+1515+## DESCRIPTION
1616+1717+Storm keeps unreleased notes in `.changes/*.md`, promotes them into
1818+`CHANGELOG.md`, and offers TUIs for reviewing diffs and entries. The binary
1919+is composed of self-contained subcommands that chain well inside scripts
2020+or CI jobs.
2121+2222+### GLOBAL FLAGS
2323+2424+| Flag | Description |
2525+| ----------------------- | -------------------------------------------------------- |
2626+| `--repo <path>` | Working tree to operate on (default: current directory). |
2727+| `-o`, `--output <file>` | Target changelog (default: `CHANGELOG.md`). |
2828+2929+### COMMANDS
3030+3131+#### `storm bump`
3232+3333+Calculate the next semantic version by inspecting `CHANGELOG.md`.
3434+3535+```text
3636+storm bump --bump <major|minor|patch> [--toolchain value...]
3737+```
3838+3939+##### Flags
4040+4141+| Flag | Description |
4242+| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
4343+| `--bump <type>` _(required)_ | Which semver component to increment. |
4444+| `--toolchain <value>` | Update language manifests (`Cargo.toml`, `pyproject.toml`, `package.json`, `deno.json`). Accepts explicit paths, type aliases like `cargo`/`npm`, or the literal `interactive` to launch a picker TUI. |
4545+4646+#### `storm release`
4747+4848+Promote `.changes/*.md` into the changelog and optionally tag the repo.
4949+5050+```text
5151+storm release (--version X.Y.Z | --bump <type>) [flags]
5252+```
5353+5454+##### Flags
5555+5656+| Flag | Description |
5757+| --------------------- | ----------------------------------------------------------------------------------- |
5858+| `--version <X.Y.Z>` | Explicit version for the new changelog entry. |
5959+| `--bump <type>` | Derive the version from the previous release (mutually exclusive with `--version`). |
6060+| `--date <YYYY-MM-DD>` | Override the release date (default: today). |
6161+| `--clear-changes` | Remove `.changes/*.md` files after a successful release. |
6262+| `--dry-run` | Render a preview without touching any files. |
6363+| `--tag` | Create an annotated git tag containing the release notes. |
6464+| `--toolchain <value>` | Update manifest files just like in `storm bump`. |
6565+6666+#### `storm generate`
6767+6868+Create `.changes/*.md` files from commit history, with optional TUI review.
6969+7070+```text
7171+storm generate <from> <to>
7272+storm generate --since <tag> [to]
7373+```
7474+7575+##### Flags
7676+7777+| Flag | Description |
7878+| --------------------- | ------------------------------------------------- |
7979+| `-i`, `--interactive` | Open a commit selector TUI for choosing entries. |
8080+| `--since <tag>` | Shortcut for `<from>`; defaults `<to>` to `HEAD`. |
8181+8282+#### `storm diff`
8383+8484+Side-by-side or unified diff with TUI navigation.
8585+8686+```text
8787+storm diff <from>..<to> [flags]
8888+storm diff <from> <to> [flags]
8989+```
9090+9191+| Flag | Description |
9292+| ------------------------------- | ----------------------------------------------------- |
9393+| `-f`, `--file <path>` | Restrict the diff to a single file. |
9494+| `-e`, `--expanded` | Show all unchanged lines instead of compressed hunks. |
9595+| `-v`, `--view <split\|unified>` | Rendering style (default: split). |
9696+9797+#### `storm check`
9898+9999+Verify every commit in a range has a corresponding unreleased entry.
100100+101101+```text
102102+storm check <from> <to>
103103+storm check --since <tag> [to]
104104+```
105105+106106+| Flag | Description |
107107+| --------------- | ---------------------------------------------------------- |
108108+| `--since <tag>` | Start range at the provided tag and default end to `HEAD`. |
109109+110110+Non-zero exit status indicates missing entries. Messages containing
111111+`[nochanges]` or `[skip changelog]` are ignored.
112112+113113+#### `storm unreleased`
114114+115115+Manage `.changes` entries directly.
116116+117117+##### `add`
118118+119119+```text
120120+storm unreleased add --type <kind> --summary <text> [--scope value]
121121+```
122122+123123+| Flag | Description |
124124+| --------------------------------------------------- | ------------------------------------------- |
125125+| `--type <added\|changed\|fixed\|removed\|security>` | Entry category. |
126126+| `--summary <text>` | Short human readable note. |
127127+| `--scope <value>` | Optional component indicator (e.g., `cli`). |
128128+129129+##### `list`
130130+131131+```text
132132+storm unreleased list [--json]
133133+```
134134+135135+| Flag | Description |
136136+| -------- | -------------------------------------------------- |
137137+| `--json` | Emit machine-readable JSON instead of styled text. |
138138+139139+##### `partial`
140140+141141+```text
142142+storm unreleased partial <commit-ref> [flags]
143143+```
144144+145145+| Flag | Description |
146146+| ------------------ | --------------------------------------------------- |
147147+| `--type <value>` | Override the inferred type from the commit message. |
148148+| `--summary <text>` | Override the inferred summary. |
149149+| `--scope <value>` | Optional component indicator. |
150150+151151+##### `review`
152152+153153+```text
154154+storm unreleased review
155155+```
156156+157157+Launch a Bubble Tea TUI for editing and deleting entries before release.
158158+Requires a TTY; fall back to `storm unreleased list` otherwise.
159159+160160+#### `storm version`
161161+162162+Print the current build’s version string.
163163+164164+## FILES
165165+166166+- `.changes/` — queue of unreleased entries created by `storm generate` or `storm unreleased add`.
167167+- `CHANGELOG.md` — Keep a Changelog-compatible file updated by `storm release`.
168168+169169+## SEE ALSO
170170+171171+`CHANGELOG.md`, [Keep a Changelog](https://keepachangelog.com), semantic versioning at [semver.org](https://semver.org).
-85
docs/markdown-examples.md
···11-# Markdown Extension Examples
22-33-This page demonstrates some of the built-in markdown extensions provided by VitePress.
44-55-## Syntax Highlighting
66-77-VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
88-99-**Input**
1010-1111-````md
1212-```js{4}
1313-export default {
1414- data () {
1515- return {
1616- msg: 'Highlighted!'
1717- }
1818- }
1919-}
2020-```
2121-````
2222-2323-**Output**
2424-2525-```js{4}
2626-export default {
2727- data () {
2828- return {
2929- msg: 'Highlighted!'
3030- }
3131- }
3232-}
3333-```
3434-3535-## Custom Containers
3636-3737-**Input**
3838-3939-```md
4040-::: info
4141-This is an info box.
4242-:::
4343-4444-::: tip
4545-This is a tip.
4646-:::
4747-4848-::: warning
4949-This is a warning.
5050-:::
5151-5252-::: danger
5353-This is a dangerous warning.
5454-:::
5555-5656-::: details
5757-This is a details block.
5858-:::
5959-```
6060-6161-**Output**
6262-6363-::: info
6464-This is an info box.
6565-:::
6666-6767-::: tip
6868-This is a tip.
6969-:::
7070-7171-::: warning
7272-This is a warning.
7373-:::
7474-7575-::: danger
7676-This is a dangerous warning.
7777-:::
7878-7979-::: details
8080-This is a details block.
8181-:::
8282-8383-## More
8484-8585-Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
+94
docs/quickstart.md
···11+---
22+title: Quickstart
33+outline: deep
44+---
55+66+# Quickstart
77+88+This walkthrough gets you from zero to a published changelog entry in a few
99+minutes. It mirrors the default workflow baked into the CLI.
1010+1111+## 1. Install the CLI
1212+1313+```sh
1414+go install github.com/stormlightlabs/git-storm/cmd/storm@latest
1515+```
1616+1717+Verify the binary is available:
1818+1919+```sh
2020+storm version
2121+```
2222+2323+## 2. Capture unreleased changes
2424+2525+Create a `.changes` entry manually or generate them from commits.
2626+2727+### Option A — Manual entry
2828+2929+```sh
3030+storm unreleased add \
3131+ --type added \
3232+ --scope cli \
3333+ --summary "Add bump command"
3434+```
3535+3636+### Option B — From git history
3737+3838+```sh
3939+storm generate --since v1.2.0 --interactive
4040+```
4141+4242+Use the commit selector TUI to pick which commits become entries. Storm writes
4343+Markdown files such as `.changes/2025-03-01-add-bump-command.md`.
4444+4545+## 3. Review pending entries
4646+4747+```sh
4848+storm unreleased review
4949+```
5050+5151+The Bubble Tea UI lets you edit summaries, delete noise, or mark entries as
5252+ready. In non-interactive environments, fall back to
5353+`storm unreleased list --json`.
5454+5555+## 4. Dry-run a release
5656+5757+```sh
5858+storm release --bump patch --dry-run
5959+```
6060+6161+This prints the new `CHANGELOG` section without modifying files. When the
6262+output looks right, re-run without `--dry-run`.
6363+6464+## 5. Publish and tag
6565+6666+```sh
6767+storm release --bump patch --toolchain package.json --tag
6868+```
6969+7070+- `--bump patch` derives the next version from the previous release.
7171+- `--toolchain package.json` keeps your npm manifest in sync.
7272+- `--tag` creates an annotated git tag containing the release notes.
7373+7474+Follow up with standard git commands:
7575+7676+```sh
7777+git add CHANGELOG.md package.json .changes
7878+git commit -m "Release v$(storm bump --bump patch)"
7979+git push origin main --tags
8080+```
8181+8282+## 6. Enforce entries in CI (optional)
8383+8484+```sh
8585+storm check --since v1.2.0
8686+```
8787+8888+The command exits non-zero when commits are missing `.changes` files, making it
8989+ideal for pre-merge checks.
9090+9191+## Next steps
9292+9393+- Skim the [Introduction](/introduction) to understand the design.
9494+- Explore every flag in the [manual](/manual).
+486
internal/toolchain/manifest.go
···11+package toolchain
22+33+import (
44+ "bytes"
55+ "encoding/json"
66+ "fmt"
77+ "io/fs"
88+ "os"
99+ "path/filepath"
1010+ "regexp"
1111+ "sort"
1212+ "strings"
1313+)
1414+1515+// ManifestType enumerates supported ecosystem manifests whose versions we can bump.
1616+type ManifestType string
1717+1818+const (
1919+ ManifestCargo ManifestType = "cargo"
2020+ ManifestPython ManifestType = "python"
2121+ ManifestNode ManifestType = "node"
2222+ ManifestDeno ManifestType = "deno"
2323+)
2424+2525+var manifestFilenames = map[string]ManifestType{
2626+ "cargo.toml": ManifestCargo,
2727+ "pyproject.toml": ManifestPython,
2828+ "package.json": ManifestNode,
2929+ "deno.json": ManifestDeno,
3030+}
3131+3232+var toolchainAliases = map[string]ManifestType{
3333+ "cargo": ManifestCargo,
3434+ "rust": ManifestCargo,
3535+ "cargo.toml": ManifestCargo,
3636+ "pyproject": ManifestPython,
3737+ "pyproject.toml": ManifestPython,
3838+ "python": ManifestPython,
3939+ "package": ManifestNode,
4040+ "package.json": ManifestNode,
4141+ "npm": ManifestNode,
4242+ "node": ManifestNode,
4343+ "deno": ManifestDeno,
4444+ "deno.json": ManifestDeno,
4545+}
4646+4747+var skipWalkDirs = map[string]struct{}{
4848+ ".git": {},
4949+ "node_modules": {},
5050+ "vendor": {},
5151+ "dist": {},
5252+ "target": {},
5353+ "tmp": {},
5454+}
5555+5656+// Manifest describes a discovered manifest file with its current version.
5757+type Manifest struct {
5858+ Type ManifestType
5959+ Path string
6060+ RelPath string
6161+ Version string
6262+ Name string
6363+}
6464+6565+// DisplayLabel returns a concise label for TUI listings.
6666+func (m Manifest) DisplayLabel() string {
6767+ label := m.RelPath
6868+ if m.Name != "" {
6969+ label = fmt.Sprintf("%s · %s", label, m.Name)
7070+ }
7171+ if m.Version != "" {
7272+ label = fmt.Sprintf("%s @ %s", label, m.Version)
7373+ }
7474+ return label
7575+}
7676+7777+// Discover scans the repository tree for supported manifest files.
7878+func Discover(root string) ([]Manifest, error) {
7979+ absRoot, err := filepath.Abs(root)
8080+ if err != nil {
8181+ return nil, err
8282+ }
8383+8484+ var manifests []Manifest
8585+ err = filepath.WalkDir(absRoot, func(path string, d fs.DirEntry, walkErr error) error {
8686+ if walkErr != nil {
8787+ return walkErr
8888+ }
8989+ if d.IsDir() {
9090+ if path != absRoot {
9191+ if _, skip := skipWalkDirs[strings.ToLower(d.Name())]; skip {
9292+ return filepath.SkipDir
9393+ }
9494+ }
9595+ return nil
9696+ }
9797+9898+ kind, ok := manifestFilenames[strings.ToLower(d.Name())]
9999+ if !ok {
100100+ return nil
101101+ }
102102+103103+ manifest, err := buildManifest(absRoot, path, kind)
104104+ if err != nil {
105105+ return err
106106+ }
107107+ manifests = append(manifests, manifest)
108108+ return nil
109109+ })
110110+ if err != nil {
111111+ return nil, err
112112+ }
113113+114114+ sort.Slice(manifests, func(i, j int) bool {
115115+ return manifests[i].RelPath < manifests[j].RelPath
116116+ })
117117+ return manifests, nil
118118+}
119119+120120+// ResolveTargets resolves CLI selectors into manifest targets, optionally requesting a TUI selection.
121121+func ResolveTargets(root string, selectors []string) ([]Manifest, bool, []Manifest, error) {
122122+ if len(selectors) == 0 {
123123+ return nil, false, nil, nil
124124+ }
125125+126126+ absRoot, err := filepath.Abs(root)
127127+ if err != nil {
128128+ return nil, false, nil, err
129129+ }
130130+131131+ available, err := Discover(absRoot)
132132+ if err != nil {
133133+ return nil, false, nil, err
134134+ }
135135+136136+ manifestByPath := make(map[string]Manifest)
137137+ for _, manifest := range available {
138138+ manifestByPath[filepath.Clean(manifest.Path)] = manifest
139139+ }
140140+141141+ var selected []Manifest
142142+ seen := make(map[string]struct{})
143143+ interactive := false
144144+145145+ for _, raw := range selectors {
146146+ value := strings.TrimSpace(raw)
147147+ if value == "" {
148148+ continue
149149+ }
150150+151151+ lower := strings.ToLower(value)
152152+ switch lower {
153153+ case "interactive", "tui", "select":
154154+ interactive = true
155155+ continue
156156+ }
157157+158158+ if kind, ok := toolchainAliases[lower]; ok {
159159+ matched := false
160160+ for _, manifest := range available {
161161+ if manifest.Type == kind {
162162+ key := filepath.Clean(manifest.Path)
163163+ if _, exists := seen[key]; !exists {
164164+ selected = append(selected, manifest)
165165+ seen[key] = struct{}{}
166166+ }
167167+ matched = true
168168+ }
169169+ }
170170+ if !matched {
171171+ return nil, false, nil, fmt.Errorf("no %s manifest found", value)
172172+ }
173173+ continue
174174+ }
175175+176176+ target := value
177177+ if !filepath.IsAbs(value) {
178178+ target = filepath.Join(absRoot, value)
179179+ }
180180+ target = filepath.Clean(target)
181181+182182+ manifest, err := loadManifest(absRoot, target)
183183+ if err != nil {
184184+ return nil, false, nil, err
185185+ }
186186+187187+ if _, exists := seen[target]; !exists {
188188+ selected = append(selected, manifest)
189189+ seen[target] = struct{}{}
190190+ }
191191+ }
192192+193193+ return selected, interactive, available, nil
194194+}
195195+196196+// UpdateManifest rewrites the manifest on disk with the provided version.
197197+func UpdateManifest(manifest Manifest, newVersion string) error {
198198+ switch manifest.Type {
199199+ case ManifestCargo:
200200+ return updateTomlVersion(manifest.Path, []string{"package"}, newVersion)
201201+ case ManifestPython:
202202+ return updateTomlVersion(manifest.Path, []string{"project", "tool.poetry"}, newVersion)
203203+ case ManifestNode:
204204+ return updateJSONVersion(manifest.Path, newVersion)
205205+ case ManifestDeno:
206206+ return updateJSONVersion(manifest.Path, newVersion)
207207+ default:
208208+ return fmt.Errorf("unsupported manifest type: %s", manifest.Type)
209209+ }
210210+}
211211+212212+func buildManifest(root, path string, kind ManifestType) (Manifest, error) {
213213+ version, name, err := extractMetadata(path, kind)
214214+ if err != nil {
215215+ return Manifest{}, err
216216+ }
217217+218218+ rel, err := filepath.Rel(root, path)
219219+ if err != nil {
220220+ rel = path
221221+ }
222222+223223+ return Manifest{
224224+ Type: kind,
225225+ Path: filepath.Clean(path),
226226+ RelPath: filepath.Clean(rel),
227227+ Version: version,
228228+ Name: name,
229229+ }, nil
230230+}
231231+232232+func loadManifest(root, path string) (Manifest, error) {
233233+ info, err := os.Stat(path)
234234+ if err != nil {
235235+ return Manifest{}, fmt.Errorf("unable to read %s: %w", path, err)
236236+ }
237237+ if info.IsDir() {
238238+ return Manifest{}, fmt.Errorf("%s is a directory", path)
239239+ }
240240+241241+ kind, ok := manifestFilenames[strings.ToLower(filepath.Base(path))]
242242+ if !ok {
243243+ return Manifest{}, fmt.Errorf("unsupported toolchain file: %s", filepath.Base(path))
244244+ }
245245+246246+ return buildManifest(root, path, kind)
247247+}
248248+249249+func extractMetadata(path string, kind ManifestType) (string, string, error) {
250250+ switch kind {
251251+ case ManifestCargo:
252252+ return parseTomlManifest(path, []string{"package"})
253253+ case ManifestPython:
254254+ return parseTomlManifest(path, []string{"project", "tool.poetry"})
255255+ case ManifestNode:
256256+ return parseJSONManifest(path)
257257+ case ManifestDeno:
258258+ return parseJSONManifest(path)
259259+ default:
260260+ return "", "", fmt.Errorf("unsupported manifest type: %s", kind)
261261+ }
262262+}
263263+264264+func parseJSONManifest(path string) (string, string, error) {
265265+ data, err := os.ReadFile(path)
266266+ if err != nil {
267267+ return "", "", err
268268+ }
269269+270270+ var payload map[string]any
271271+ if err := json.Unmarshal(data, &payload); err != nil {
272272+ return "", "", fmt.Errorf("failed to parse %s: %w", filepath.Base(path), err)
273273+ }
274274+275275+ version, _ := payload["version"].(string)
276276+ if version == "" {
277277+ return "", "", fmt.Errorf("version not found in %s", filepath.Base(path))
278278+ }
279279+280280+ name, _ := payload["name"].(string)
281281+ return version, name, nil
282282+}
283283+284284+func parseTomlManifest(path string, sections []string) (string, string, error) {
285285+ data, err := os.ReadFile(path)
286286+ if err != nil {
287287+ return "", "", err
288288+ }
289289+290290+ sectionSet := make(map[string]struct{})
291291+ for _, section := range sections {
292292+ sectionSet[section] = struct{}{}
293293+ }
294294+295295+ var current string
296296+ var version string
297297+ var name string
298298+ lines := strings.Split(string(data), "\n")
299299+300300+ for _, line := range lines {
301301+ trimmed := strings.TrimSpace(line)
302302+ if trimmed == "" || strings.HasPrefix(trimmed, "#") {
303303+ continue
304304+ }
305305+ if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
306306+ current = strings.TrimSpace(strings.Trim(trimmed, "[]"))
307307+ continue
308308+ }
309309+ if _, ok := sectionSet[current]; !ok {
310310+ continue
311311+ }
312312+313313+ if value, ok := parseTomlAssignment(line, "version"); ok && version == "" {
314314+ version = value
315315+ continue
316316+ }
317317+ if value, ok := parseTomlAssignment(line, "name"); ok && name == "" {
318318+ name = value
319319+ }
320320+ }
321321+322322+ if version == "" {
323323+ return "", "", fmt.Errorf("version not found in %s", filepath.Base(path))
324324+ }
325325+326326+ return version, name, nil
327327+}
328328+329329+func parseTomlAssignment(line, key string) (string, bool) {
330330+ withoutComment := strings.Split(line, "#")[0]
331331+ parts := strings.SplitN(withoutComment, "=", 2)
332332+ if len(parts) != 2 {
333333+ return "", false
334334+ }
335335+ if strings.TrimSpace(parts[0]) != key {
336336+ return "", false
337337+ }
338338+339339+ value := strings.TrimSpace(parts[1])
340340+ if len(value) >= 2 {
341341+ if (value[0] == '"' && value[len(value)-1] == '"') || (value[0] == '\'' && value[len(value)-1] == '\'') {
342342+ value = value[1 : len(value)-1]
343343+ }
344344+ }
345345+ return value, true
346346+}
347347+348348+var tomlVersionPattern = regexp.MustCompile(`^(\s*version\s*=\s*)(['"])([^'"]*)(['"])(.*)$`)
349349+350350+func updateTomlVersion(path string, sections []string, newVersion string) error {
351351+ data, err := os.ReadFile(path)
352352+ if err != nil {
353353+ return err
354354+ }
355355+356356+ sectionSet := make(map[string]struct{})
357357+ for _, section := range sections {
358358+ sectionSet[section] = struct{}{}
359359+ }
360360+361361+ lines := strings.Split(string(data), "\n")
362362+ current := ""
363363+ replaced := false
364364+365365+ for idx, line := range lines {
366366+ trimmed := strings.TrimSpace(line)
367367+ if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
368368+ current = strings.TrimSpace(strings.Trim(trimmed, "[]"))
369369+ continue
370370+ }
371371+ if replaced {
372372+ continue
373373+ }
374374+ if _, ok := sectionSet[current]; !ok {
375375+ continue
376376+ }
377377+ if matches := tomlVersionPattern.FindStringSubmatch(line); matches != nil {
378378+ lines[idx] = fmt.Sprintf("%s%s%s%s%s", matches[1], matches[2], newVersion, matches[4], matches[5])
379379+ replaced = true
380380+ }
381381+ }
382382+383383+ if !replaced {
384384+ return fmt.Errorf("version not found in %s", filepath.Base(path))
385385+ }
386386+387387+ return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
388388+}
389389+390390+func updateJSONVersion(path string, newVersion string) error {
391391+ data, err := os.ReadFile(path)
392392+ if err != nil {
393393+ return err
394394+ }
395395+396396+ start, end, err := findRootJSONVersion(data)
397397+ if err != nil {
398398+ return fmt.Errorf("version not found in %s", filepath.Base(path))
399399+ }
400400+401401+ var buf bytes.Buffer
402402+ buf.Grow(len(data) - (end - start) + len(newVersion))
403403+ buf.Write(data[:start])
404404+ buf.WriteString(newVersion)
405405+ buf.Write(data[end:])
406406+407407+ return os.WriteFile(path, buf.Bytes(), 0644)
408408+}
409409+410410+func findRootJSONVersion(data []byte) (int, int, error) {
411411+ depth := 0
412412+ inString := false
413413+ escape := false
414414+ keyStart := -1
415415+416416+ for i := 0; i < len(data); i++ {
417417+ b := data[i]
418418+ if inString {
419419+ if escape {
420420+ escape = false
421421+ continue
422422+ }
423423+ if b == '\\' {
424424+ escape = true
425425+ continue
426426+ }
427427+ if b == '"' {
428428+ inString = false
429429+ keyEnd := i
430430+ if keyStart >= 0 {
431431+ j := i + 1
432432+ for j < len(data) && (data[j] == ' ' || data[j] == '\t' || data[j] == '\n' || data[j] == '\r') {
433433+ j++
434434+ }
435435+ if j < len(data) && data[j] == ':' {
436436+ key := string(data[keyStart:keyEnd])
437437+ if key == "version" && depth == 1 {
438438+ valueStart, valueEnd, err := locateJSONString(data, j+1)
439439+ if err != nil {
440440+ return -1, -1, err
441441+ }
442442+ return valueStart, valueEnd, nil
443443+ }
444444+ }
445445+ }
446446+ keyStart = -1
447447+ }
448448+ continue
449449+ }
450450+451451+ switch b {
452452+ case '"':
453453+ inString = true
454454+ keyStart = i + 1
455455+ case '{', '[':
456456+ depth++
457457+ case '}', ']':
458458+ if depth > 0 {
459459+ depth--
460460+ }
461461+ }
462462+ }
463463+464464+ return -1, -1, fmt.Errorf("version key not found")
465465+}
466466+467467+func locateJSONString(data []byte, start int) (int, int, error) {
468468+ i := start
469469+ for i < len(data) && (data[i] == ' ' || data[i] == '\t' || data[i] == '\n' || data[i] == '\r') {
470470+ i++
471471+ }
472472+ if i >= len(data) || data[i] != '"' {
473473+ return -1, -1, fmt.Errorf("version value must be a string")
474474+ }
475475+ valueStart := i + 1
476476+ for j := valueStart; j < len(data); j++ {
477477+ if data[j] == '\\' {
478478+ j++
479479+ continue
480480+ }
481481+ if data[j] == '"' {
482482+ return valueStart, j, nil
483483+ }
484484+ }
485485+ return -1, -1, fmt.Errorf("unterminated version string")
486486+}