AGENTS.md

  1<!--
  2SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  3
  4SPDX-License-Identifier: CC0-1.0
  5-->
  6
  7# wt, Lua worktree manager
  8
  9## Commands
 10- `make all` — format, lint, type-check, and test (the standard workflow)
 11- `make test-quiet`**use this!** shows summary on success, full output on failure
 12- `make test` — verbose test output (busted)
 13- `lx test spec/foo_spec.lua` — run a single test file
 14- `lx run` — run the application (src/main.lua)
 15- `make dist` — bundle all modules into `dist/wt` single-file executable
 16- `make install` — install to `~/.local/bin/`
 17- `make ci` — CI mode (format check + lint + check + test)
 18
 19## Structure
 20```
 21src/
 22├── main.lua           # Entry point, command dispatch
 23└── wt/
 24    ├── exit.lua       # Exit codes
 25    ├── shell.lua      # run_cmd, run_cmd_silent, die, quote
 26    ├── path.lua       # Path manipulation (branch_to_path, etc.)
 27    ├── git.lua        # Git operations, project root detection
 28    ├── config.lua     # Config loading (global + project)
 29    ├── hooks.lua      # Hook permissions and execution
 30    ├── help.lua       # Usage text
 31    └── cmd/           # One module per command
 32spec/                  # Tests (busted framework, *_spec.lua)
 33scripts/bundle.lua     # Builds single-file dist/wt
 34```
 35
 36## Lux Handles These (don't suggest manually)
 37- Dependencies: use `lx add <pkg>`, not `luarocks install`
 38- Tools (busted, luacheck, stylua): auto-installed on first use
 39- Module paths: Lux loader resolves requires; don't manipulate `package.path`
 40
 41## Code Style
 42- Lua 5.4; use LuaCATS annotations (`---@param`, `---@return`, `---@class`)
 43- Tabs for indentation, 120 char line limit (see stylua.toml)
 44- Prefix intentionally unused variables with `_` to silence luacheck
 45
 46## Architecture
 47
 48### Core Pattern: Bare Repo + Worktrees
 49wt manages git repositories using a **bare repository structure**:
 50- `.bare/` - the actual git repository (moved from `.git/`)
 51- `.git` - a file pointing to `.bare/` (makes git commands work)
 52- Worktrees checked out as sibling directories alongside `.bare/`
 53
 54### Module Organization
 55Each command lives in `src/wt/cmd/<name>.lua` and exports a `cmd_<name>(args)` function. Commands:
 561. Parse flags and positional args from the `args` table
 572. Validate inputs with `shell.die(msg)` for user errors
 583. Execute via `shell.run_cmd()` / `shell.run_cmd_silent()`
 594. Return consistent exit codes from `wt.exit`
 60
 61Utility modules (`shell`, `git`, `path`, `config`, `hooks`) are shared across commands.
 62
 63### Exit Codes and Error Handling
 64```lua
 65local exit = require("wt.exit")
 66exit.EXIT_SUCCESS      -- 0: normal completion
 67exit.EXIT_USER_ERROR   -- 1: user mistakes (bad args, conflicts)
 68exit.EXIT_SYSTEM_ERROR -- 2: system failures (git errors, IO errors)
 69```
 70
 71Use `shell.die(msg, code)` to print to stderr and exit. Default code is `EXIT_USER_ERROR`.
 72
 73### Git Command Patterns
 74- **Bare repo operations**: `GIT_DIR=<path>/.bare git <command>`
 75- **Worktree operations**: `git -C <path> <command>`
 76- Always check exit codes from `run_cmd()` before assuming success
 77- Use `shell.quote(str)` when embedding user input in commands
 78
 79### Testing
 80
 81Tests use busted framework in `spec/`. Key patterns:
 82
 83```lua
 84-- Load wt as module (exports functions when required, not run directly)
 85package.path = package.path .. ";./?.lua"
 86local wt = dofile("src/main.lua")
 87
 88-- Use spec/test_helper.lua for git operations
 89local helper = require("spec.test_helper")
 90local git = helper.git  -- Preconfigured git wrapper (suppresses noise)
 91
 92-- Standard setup/teardown for temp directories
 93local temp_dir
 94setup(function()
 95    local handle = io.popen("mktemp -d")
 96    if handle then
 97        temp_dir = handle:read("*l")
 98        handle:close()
 99    end
100end)
101teardown(function()
102    if temp_dir then os.execute("rm -rf " .. temp_dir) end
103end)
104```
105
106The `dofile("src/main.lua")` pattern works because main.lua detects when it's `require`d vs run directly and exports test utilities.
107
108### Distribution
109
110`make dist` bundles all modules into a single `dist/wt` file using `scripts/bundle.lua`. The bundler:
111- Embeds each module as a string literal
112- Provides custom `require()` that loads embedded modules first
113- Falls back to real `require()` for stdlib
114
115**Order matters**: Update `MODULE_ORDER` in `scripts/bundle.lua` when adding new modules (dependencies before dependents).
116
117## Non-Obvious Behaviors
118
1191. **Source Worktree Detection**: `wt a` must run from inside a worktree (not project root) for hooks to trigger.
1202. **Idempotent Add**: Running `wt a <branch>` when worktree exists prints path and succeeds (exit 0), with hints if run from root.
1213. **Branch Existence Checking**: Adding without `-b` checks both local and all remotes. Fails if branch exists on multiple remotes (ambiguous).
1224. **Bare HEAD Management**: During `wt init`, bare repo's HEAD moves to placeholder so default branch can be checked out in worktree.
1235. **Config File Format**: Both `return { ... }` and bare table `{ ... }` are accepted (loader tries prepending `return`).
1246. **Test Module Export**: `src/main.lua` uses `pcall(debug.getlocal, 4, 1)` to detect if it was `require`d and exports internal functions for testing.
125
126## External Dependencies
127- `gum` - interactive prompts (choose, confirm)
128- Standard POSIX: `mkdir`, `cp`, `ln`, `rm`, `mv`, `pwd`, `mktemp`
129- git