7.6 KiB
7.6 KiB
AGENT Instructions for veans
Things to know before touching this submodule that aren't obvious from
reading the code. The parent repo's CLAUDE.md covers the rest of Vikunja;
this file is veans-specific.
Module layout
veans/is its own Go module (code.vikunja.io/veans), separate from the parent. Don't try to importcode.vikunja.io/api/...— that pulls XORM into the CLI binary. Wire types live ininternal/client/types.goas plain JSON-tagged structs that mirror the parent models.- License headers are enforced by
goheaderinveans/.golangci.yml. Every new.gofile needs the AGPLv3 banner fromveans/code-header-template.txt(a copy of the parent's, kept local so the linter resolves the path relative to this module).
Building and testing
mage build→./veansbinary. TheAliasesmap inmagefile.goroutes bare names likemage testtoTest.All— without aliases, mage rejects namespace invocations ("Unknown target specified").- Unit tests:
mage testorgo test ./.... - E2e tests: assume an externally-running Vikunja at
VEANS_E2E_API_URLand admin creds in env (VEANS_E2E_ADMIN_TOKEN, orVEANS_E2E_ADMIN_USER+VEANS_E2E_ADMIN_PASS). The package self-skips whenVEANS_E2E_API_URLis empty, so plaingo testis safe locally. - Local e2e loop: from the parent repo root, build the API
(
mage build:build), run it with sqlite-memory + a known JWT secret, register an admin user viaPOST /register, thengo test ./e2e/...fromveans/with the env vars above. - CI: the
test-veans-e2ejob in.github/workflows/test.ymlconsumes the existingvikunja_binartifact fromapi-build; don't recompile the API in a parallel workflow. Theveans-testjob runs unit tests independently and gives fast feedback.
Vikunja wire-format gotchas
Most failures surface when crossing the JSON boundary. The list below is what's bitten me; if a new endpoint behaves oddly, suspect one of these:
ProjectView.view_kindandbucket_configuration_modeare strings, not ints. The parent enums (ProjectViewKind,BucketConfigurationModeKind) have customMarshalJSONthat emits"kanban"/"manual"etc. Use the string constants ininternal/client/types.go.Task.BucketIDis always 0 inGET /tasks/:id. The model hasxorm:"-"on it — the actual bucket lives in a separatetask_bucketstable. Fetch with?expand=bucketsand usetask.CurrentBucketID(viewID)to read it.POST /tasks/{id}does NOT move tasks between buckets. The task↔bucket relation is row-shaped; useclient.MoveTaskToBucket()which hitsPOST /projects/{p}/views/{v}/buckets/{b}/tasks. The Update path on the server only auto-moves ondoneflips.- Bot user creation is
PUT /user/bots, not/bots— the routes are registered under the/usersubgroup. Same prefix forGET /user/bots. APIToken.expires_atis required. The struct field hasvalid:"required"upstream; sending it omitted or zero fails validation. Useclient.FarFuture(year 9999) when you mean "no expiry" — the frontend does the same.
API token permissions
- Vikunja validates token
permissionsagainstapiTokenRoutes, a map built dynamically from registered routes. Group names are derived from the URL path (params stripped, joined by_). Examples:/projects/:project/views/:view/buckets/:bucket/tasks→ groupprojects, actionviews_buckets_tasks/tasks/:task/comments→ grouptasks_comments, actioncreate
client.PermissionsForBot()callsGET /routesat runtime and grants only the intersection of what we want and what the server exposes. Don't hard-code permission group names — they drift across Vikunja versions, and discovery keeps the bot's grant valid across upgrades.
Bot ownership and token minting
- Creating a bot via
PUT /user/botsautomatically sets the bot'sbot_owner_idto the calling user. Only the owner can mint tokens for the bot viaPUT /tokenswithowner_id=<bot_id>. The init flow does these as a single human-JWT-authenticated batch. - Bots have no password and cannot authenticate via
POST /login. After init,veans loginre-authenticates as the human (not the bot) and mints a fresh bot token.
OAuth flow
- Vikunja's authorization server requires PKCE/S256 and accepts either
vikunja-…://custom schemes or RFC 8252 loopback URIs (http://127.0.0.1:NNN/,http://localhost:NNN/,http://[::1]:NNN/). No client registration needed —client_idcan be any consistent string (we useveans-cli). internal/auth/oauth.gobinds a free port on 127.0.0.1, opens the browser, and captures the callback. TheShutdowndefer usescontext.WithoutCancel(ctx)so cancellation at the outer scope still drains the loopback server cleanly.- Token exchange is JSON only. Form-encoded POSTs to
/oauth/tokenfail; the standardgolang.org/x/oauth2client speaks form encoding, which is why we have a hand-rolledclient.ExchangeOAuthCode.
Credential store
- Lookup chain: keychain → env (
VEANS_TOKEN, optionally pinned byVEANS_SERVER) → file (~/.config/veans/credentials.yml, mode 0600, honorsXDG_CONFIG_HOME). Chain.Setfalls through to the next backend on error so a missing dbus on a CI runner doesn't block writes — the file backend is the reliable last-resort.- E2e tests override
HOMEandXDG_CONFIG_HOMEper test to keep the developer's keyring untouched. Don't bypass the credentials package in tests — leaks between tests will surface as the wrong bot token.
Project identifiers and bot usernames
- Project
Identifierisrunelength(0|10), can be empty. When empty,Config.FormatTaskIDrenders#NN; otherwisePROJ-NN. Both are accepted byruntime.resolveTaskIDalong with bare integers. - Bot username must start with
bot-; the server enforces it. Hyphens, digits, lowercase letters allowed; no spaces, no commas, nolink-share-Npattern.config.SuggestedBotUsernamedoes the folding for repo names. - E2e tests deriving identifiers from a unique suffix should use the
trailing chars of
strconv.FormatInt(time.Now().UnixNano(), 36). The leading chars barely change between consecutive runs and will collide if you take[:N].
Cobra surface conventions
- Global
--jsonflag flips both successful output (raw object forshow, raw array forlist) and the error envelope. - Stable error codes in
internal/output/errors.go. Don't add new ad-hoc strings — wrap withoutput.New/output.Wrap. RunEhandlers that don't useargs []stringshould rename it to_to satisfy revive'sunused-parameterrule.- The bucket-move dance (
MoveTaskToBucket) runs after the field update onupdate, so a status transition can't clobber freshly attached labels. Comments for--status scrappedpost before the bucket move so the audit trail reads in chronological order.
Things to not do
- Don't add an
os/exec.Commandwithout ctx —noctxis enabled. Useexec.CommandContext(ctx, …)and thread the context through. - Don't commit the built binary.
veans/.gitignorecovers./veansand./veans.exe. - Don't write to stdout from
primewhen no.veans.ymlis found. The hook contract is silent + exit 0 so the snippet is safe to install globally in~/.claude/settings.json. - Don't change canonical bucket titles without updating
internal/status/CanonicalBucketTitles, the prompt template, and the e2e assertions in lockstep — agents and humans both treat them as fixed strings.