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
2 changes: 1 addition & 1 deletion .agents/skills/verify-diff-lifecycle/covered.lock
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ site lua/claudecode/diff_inline.lua: resolution_callback = resolution_callback,
== lua/claudecode/health.lua
file lua/claudecode/health.lua 5ab73bf388f3ac97
== lua/claudecode/init.lua
file lua/claudecode/init.lua dc27182999586381
file lua/claudecode/init.lua ee4999490093989d
block user_close_all 10e024fd8ada1c16
site lua/claudecode/init.lua: local pending = diff.close_pending_diffs("user command") @ function M._create_commands()
site lua/claudecode/init.lua: local rejected = diff.close_rejected_diffs("user command") @ function M._create_commands()
Expand Down
47 changes: 4 additions & 43 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,50 +370,11 @@ Log levels for authentication events:

## Release Process

### Version Updates
release-please runs releases (`.github/workflows/release-please.yaml`). Each push to `main` runs release-please over the commits since the last release. When any of them would appear in the release notes, for example a `feat` or `fix` commit, it opens or updates the release PR, titled `chore(release): x.y.z`. The release PR bumps `VERSION` and `.release-please-manifest.json`, and Communique regenerates `CHANGELOG.md` and the PR body. Merging that PR creates the `vX.Y.Z` tag and the GitHub Release.

When updating the version number for a new release, you must update **ALL** of these files:

1. **`lua/claudecode/init.lua`** - Main version table:

```lua
M.version = {
major = 0,
minor = 2, -- Update this
patch = 0, -- Update this
prerelease = nil, -- Remove for stable releases
}
```

2. **`scripts/claude_interactive.sh`** - Multiple client version references:
- Line ~52: `"version": "0.2.0"` (handshake)
- Line ~223: `"version": "0.2.0"` (initialize)
- Line ~309: `"version": "0.2.0"` (reconnect)

3. **`scripts/lib_claude.sh`** - ClaudeCodeNvim version:
- Line ~120: `"version": "0.2.0"` (init message)

4. **`CHANGELOG.md`** - Add new release section with:
- Release date
- Features with PR references
- Bug fixes with PR references
- Development improvements

### Release Commands

```bash
# Get merged PRs since last version
gh pr list --state merged --base main --json number,title,mergedAt,url --jq 'sort_by(.mergedAt) | reverse'

# Get commit history
git log --oneline v0.1.0..HEAD

# Always run before committing
mise run all

# Verify no old version references remain
rg "0\.1\.0" . # Should only show CHANGELOG.md historical entries
```
- Do not edit the version by hand. `lua/claudecode/init.lua` reads `M.version` from `VERSION` when it loads, so a release changes no Lua file and `covered.lock` stays current. The numbers in `init.lua` are only the `0.0.0-unknown` fallback.
- Give each PR a Conventional Commits title. The merge queue squashes with the title, and release-please picks the next version from those subjects.
- The `"version"` strings in `scripts/claude_interactive.sh` and `scripts/lib_claude.sh` are the test client's own `clientInfo`, not the plugin version.

## Development Workflow

Expand Down
2 changes: 1 addition & 1 deletion lua/claudecode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Module map (direct files):
- `M.is_claude_connected()`: false when the server is not running. When `get_status().clients` is non-empty, true only when a client has `handshake_complete`. When it is empty, true when `client_count > 0`, with no handshake check [init.lua:M.is_claude_connected]
- `M.send_at_mention(file_path, start_line?, end_line?, context?) -> ok, err?`: line numbers are 0-indexed [init.lua:M.send_at_mention]
- `M.process_mention_queue(from_new_connection?)`: the server's `on_connect` callback calls it [init.lua:M.process_mention_queue]
- `M.open_with_model(args)`, `M.get_version()`, and `M.version` (`0.2.0`) [init.lua:M.version]
- `M.open_with_model(args)`, `M.get_version()`, and `M.version`, read from the repository's `VERSION` file at load [init.lua:M.version]
- Internal API: `M._create_commands`, `M._broadcast_at_mention`, `M._add_paths_to_claude` (default `max_files` 100, `batch_size` 10), `M._format_path_for_at_mention`, `M._ensure_terminal_visible_if_connected`, and `M._maybe_warn_unfocusable_provider` [init.lua:M._add_paths_to_claude]

### User commands [init.lua:M._create_commands]
Expand Down
21 changes: 18 additions & 3 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ local M = {}

local logger = require("claudecode.logger")

--- Current plugin version
--- Current plugin version. The values come from the VERSION file at the repository root, which release-please
--- bumps, so a release changes no Lua file. Without a readable VERSION file the version is 0.0.0-unknown.
---@type ClaudeCodeVersion
M.version = {
major = 0,
minor = 2,
minor = 0,
patch = 0,
prerelease = nil,
prerelease = "unknown",
string = function(self)
local version = string.format("%d.%d.%d", self.major, self.minor, self.patch)
if self.prerelease then
Expand All @@ -25,6 +26,20 @@ M.version = {
end,
}

do
local root = debug.getinfo(1, "S").source:match("^@(.-)lua[/\\]claudecode[/\\]init%.lua$")
local file = root and io.open(root .. "VERSION", "r")
if file then
local text = file:read("*l") or ""
file:close()
local major, minor, patch, prerelease = text:match("^(%d+)%.(%d+)%.(%d+)%-?(%S*)%s*$")
if major then
M.version.major, M.version.minor, M.version.patch = tonumber(major), tonumber(minor), tonumber(patch)
M.version.prerelease = prerelease ~= "" and prerelease or nil
end
end
end

-- Module state
---@type ClaudeCodeState
M.state = {
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/version_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require("tests.busted_setup")
require("tests.mocks.vim")

local temp_dir = require("tests.helpers.temp_dir")

---Load a copy of init.lua from <dir>/lua/claudecode/init.lua, so it reads <dir>/VERSION.
---@param dir string
---@param version_text string|nil Contents of <dir>/VERSION, or nil for no file
local function load_copy(dir, version_text)
os.execute("mkdir -p " .. temp_dir.quote(dir .. "/lua/claudecode"))
local src = assert(io.open("lua/claudecode/init.lua", "r"))
local dst = assert(io.open(dir .. "/lua/claudecode/init.lua", "w"))
dst:write(src:read("*a"))
src:close()
dst:close()
if version_text then
local file = assert(io.open(dir .. "/VERSION", "w"))
file:write(version_text)
file:close()
end
return assert(loadfile(dir .. "/lua/claudecode/init.lua"))()
end

describe("claudecode.version", function()
local dir

before_each(function()
dir = temp_dir.new()
end)

after_each(function()
temp_dir.remove(dir)
dir = nil
end)

it("matches the VERSION file that release-please bumps", function()
package.loaded["claudecode"] = nil
local claudecode = require("claudecode")
local file = assert(io.open("VERSION", "r"), "run the tests from the repository root")
local expected = file:read("*l")
file:close()
assert.are.equal(expected, claudecode.version:string())
end)

it("reads a prerelease from VERSION", function()
local version = load_copy(dir, "1.12.3-rc.1\n").version
assert.are.same({ 1, 12, 3, "rc.1" }, { version.major, version.minor, version.patch, version.prerelease })
assert.are.equal("1.12.3-rc.1", version:string())
end)

it("reports 0.0.0-unknown without a VERSION file", function()
assert.are.equal("0.0.0-unknown", load_copy(dir, nil).version:string())
end)
end)
Loading