Commit Graph

56 Commits

Author SHA1 Message Date
renovate[bot] 8a4a1c1af7 fix(deps): update module golang.org/x/term to v0.44.0 2026-06-30 03:00:28 +00:00
kolaente ee8c759f0b
chore(deps): update go to 1.26.4 in all places 2026-06-29 13:12:57 +02:00
renovate[bot] 59ef240a4d fix(deps): update module github.com/sahilm/fuzzy to v0.1.3 2026-06-29 08:13:58 +02:00
kolaente 7691f282cf fix(veans): preserve unsent task fields on update via PATCH (#2962)
The v1 update was a whole-object POST /tasks/{id}: omitted scalars were
zeroed, so a status-only `veans update` silently wiped a task's
description and priority. The v1->v2 migration replaced that with
PATCH /tasks/{id} carrying a JSON Merge Patch built from only the
changed fields (client.TaskPatch, all-pointer + omitempty), which fixes
this by construction — absent fields are left untouched server-side.

Pin it with the acceptance tests from the issue: a title-only and a
status-only update must send only the field(s) they change, so the
stored description and priority survive.
2026-06-26 11:23:14 +00:00
kolaente 6cee626383 refactor(veans): migrate API client from v1 to v2
veans is unreleased and targets bleeding-edge Vikunja, so the CLI now
speaks the Huma-backed /api/v2 exclusively (v1 is frozen and the kanban
bucket CRUD veans relies on only exists on v2).

- Transport: base path /api/v1 -> /api/v2 in Do/DoRaw; add a
  content-type-aware path (DoMerge for application/merge-patch+json).
- Pagination: drop the x-pagination-total-pages header reader; every v2
  list returns the {items,total,page,per_page,total_pages} envelope.
  Decode it with a generic Paginated[T]/doList[T] and page until
  page >= total_pages. Previously-single-GET lists (views, buckets,
  comments, bots) are enveloped too — unwrap .items.
- Verbs: creates flip PUT -> POST (projects, labels, tokens, bot users,
  shares, task create, comments, relations, assignees, label-attach,
  bucket create); the bucket-task move flips POST -> PUT with a bare
  {"task_id":N} body (URL owns project/view/bucket); task update moves
  to PATCH merge-patch with a partial body.
- Errors: parse the RFC 9457 problem+json body (detail/title/code)
  instead of v1's {code,message}; the status -> output.Code mapping is
  unchanged.
- Discovery probes /api/v2/info, which doubles as the "new enough" check.
- Label search param s -> q; add views_buckets_tasks_put to the bot's
  projects scope so the move is authorized regardless of route-init order.

Tests and the veans agent guide are updated for the new paths, verbs and
envelope. Verified end-to-end against a local v2 server: init, create,
show, list, claim, update and prime all work.
2026-06-26 11:23:14 +00:00
kolaente e271f75cad feat(init): use the hierarchical fuzzy picker for project selection
Replaces the flat numbered project list during 'veans init' with the interactive picker. --project <id> still bypasses it; non-TTY stdin fails cleanly asking for --project.
2026-06-10 13:51:46 +02:00
kolaente 3462e24ec7 feat(picker): add hierarchical fuzzy project picker
Interactive bubbletea picker that renders projects as an indented tree (siblings by position then title, orphans re-parented to root) and fuzzy-filters as you type, keeping matched rows' ancestors visible as dimmed context. Pure tree/flatten logic is split from the TUI and unit-tested.
2026-06-10 13:51:46 +02:00
kolaente a221a15ec3 feat(client): add parent_project_id and position to Project wire type
The init project picker needs the parent/child relationship and sibling ordering to render projects hierarchically like the web sidebar.
2026-06-10 13:51:46 +02:00
kolaente 304fe55da7 refactor: drop Release.* from project magefiles and point Dockerfile at build/
The release pipeline lives entirely in build/magefile.go now, so the
per-project Release namespaces in vikunja's magefile.go and
veans/magefile.go are dead weight. Drop them.

Update the Dockerfile in the same commit so the apibuilder stage
invokes `cd build && mage release:xgo vikunja <target>` — the parent
magefile no longer exposes that target.
2026-05-27 13:01:44 +00:00
kolaente 5f00fca166 feat(veans): build and publish veans alongside vikunja
Cross-compile veans for the same OS/arch matrix as the main vikunja
binary, wrap each into a signed zip, build deb/rpm/apk/archlinux
packages via nfpm, and merge those into the existing dl.vikunja.io
package repos so `apt install veans` works from the same source.

- veans/magefile.go: Release namespace (xgo cross-compile, upx, sha256,
  per-target zip bundle, nfpm.yaml templating).
- veans/nfpm.yaml: minimal — binary at /usr/local/bin/veans, no service
  or postinstall.
- .github/workflows/release.yml: veans-binaries + veans-os-package
  jobs, veans artifacts merged into publish-repos and create-release.
S3 layout mirrors vikunja under /veans/<version>/.
2026-05-27 13:01:44 +00:00
Tink bot 98affb265a test(veans): cover bootstrap validators and prompt UX
Three helpers I added recently have no e2e coverage because the
suite always passes --bot-username with a valid name and
--yes-buckets to skip prompts.

Nine tests in a new bootstrap_test.go:

- TestValidateBotUsername — table-driven, 18 rows: valid shapes
  (bot-foo, bot-foo-bar, bot-foo123, bot-foo_bar, bot-foo.bar,
  bot-a), invalid prefix (foo, Bot-foo, ""), invalid chars
  (spaces, commas, uppercase, !, embedded space), the reserved
  link-share-N pattern, and the bare "bot-" edge.

- TestConfirmOverwriteExistingConfig — file-missing path, the
  OverwriteExistingConfig=true short-circuit, every interesting
  prompt answer (y, yes, Y, Yes, "  yes  " → proceed; n, "",
  garbage → CodeConflict with path in message; prompter error
  → CodeUnknown wrapping the original via errors.Is).

- TestBootstrapBuckets_{AllPresent,AutoApprove,PromptDeclined,
  PromptAborted,PromptUnknownCap,PromptAccepted} — drive the
  function against a stub httptest server (bucketServer helper)
  that records ListBuckets responses and CreateBucket payloads,
  with a scripted queuePrompter for the prompt-driven cases.
  Covers the alias-match short circuit, the auto-approve path,
  the new declined/aborted/retry-cap paths, and the y-accepted
  path.

Local helpers (queuePrompter for scripted answers with injectable
error; bucketServer for the stubbed bucket endpoints) stay in the
test file — no production code changes.
2026-05-27 08:21:57 +00:00
Tink bot 964fdb71d1 test(veans): cover OAuth callback handler error paths
The e2e suite bypasses the OAuth flow via --token, so the callback
handler's error branches had zero coverage. Eight tests appended to
oauth_test.go drive the handler directly:

- happy path: code+state arrive on the channel; response is HTML
- authz-server error path: ?error=access_denied&error_description=…
  bubbles up as a non-nil err containing the description (not the code)
- only-code fallback: when error_description is missing, the error
  message falls back to the error code
- empty code: handler captures it; waitForCallback's job to reject
- non-GET method: 405 with Allow: GET, nothing pushed to channel
  (defense against forged POST from a same-origin page)
- wrong path: 404, nothing pushed
- HTML-escaping: an error containing <script>…</script> renders as
  &lt;script&gt; — XSS regression guard
- nil-err success page: 200 with 'veans is authorized'

Plus generateState shape coverage (length, charset, uniqueness)
to match the existing TestGeneratePKCE_*.

Sanity-checked the XSS test by deleting the html.EscapeString call —
it fails with raw <script> in the body. Restored.
2026-05-27 08:21:57 +00:00
Tink bot 4cda019336 test(veans): cover client error mapping, pagination, and 404→BOT_USERS_UNAVAILABLE
internal/client/ had no coverage for the helpers that turn HTTP
responses into the stable error envelope. e2e exercises happy paths
but never asserts the envelope's Code field for each status, so a
refactor of mapHTTPError could silently drift.

Seven tests in client_test.go:

- TestMapHTTPError_StatusCodeMapping table-drives 401/403/404/409/
  429/400/422/500 → the right output.Code constants.
- TestMapHTTPError_RetryAfterAppendedToMessage asserts the
  (retry-after <dur>) suffix on 429s.
- TestMapHTTPError_BodyTruncation pumps 600 bytes and asserts the
  message ends with …(truncated) and Cause stays nil (per the earlier
  'drop synthetic Cause' change).
- TestMapHTTPError_VikunjaJSONTakesPrecedenceOverRawBody asserts the
  parsed {code,message} payload wins over the raw body for the
  embedded message text.
- TestParseRetryAfter handles delta-seconds, HTTP-date forms (with a
  tolerance window because the parser uses time.Until), and the
  unparseable/empty/negative/past-date cases.
- TestPaginationDone covers the header-authoritative and len-heuristic
  branches across full, short, and empty pages.
- TestCreateBotUser_404TranslatesToBotUsersUnavailable drives a
  fake httptest server returning 404 on PUT /api/v1/user/bots and
  asserts the error code is BOT_USERS_UNAVAILABLE (the translation
  lives in users.go:37-42).
2026-05-27 08:21:57 +00:00
Tink bot 9b95d05811 test(veans): cover the stable error-envelope contract
internal/output/ had zero unit tests; the envelope shape it produces
is consumed by every agent integration on the other side of stdin, so
locking it down with a small test file is high-leverage.

Six tests in errors_test.go:
- TestAsError_Nil, TestAsError_PreservesKnownCode,
  TestAsError_UnwrapsThroughFmtErrorf,
  TestAsError_PlainErrorBecomesUnknown — pin AsError's contract
  against nil / direct / wrapped / plain inputs.
- TestEmitError_EnvelopeShape — round-trips through bytes.Buffer and
  asserts exactly two keys ("code", "error"), correct values, and a
  trailing newline.
- TestWrap_PreservesCauseForErrorsIs — confirms errors.Is and
  errors.As walk through Wrap so future sentinel introductions work.

A comment block documents why EmitError's encode-failure fallback
isn't exercised — json.Marshal of {Code, Message} cannot fail, so the
branch is unreachable from outside the package.
2026-05-27 08:21:57 +00:00
Tink bot c715520ab9 test(veans): cover credential-store hardening invariants
Four unit tests in internal/credentials/file_test.go for behaviors that
have no e2e coverage (e2e exercises file-backend writes round-trip but
never stats the mode, never simulates a crash, never races two
processes, never observes the fallback warning):

- TestFileBackend_SetReassertsMode pre-creates the file at 0o644 and
  asserts Set narrows it to 0o600 via Chmod-after-Rename.
- TestFileBackend_SetCleansUpTmpFile scans the dir after Set and
  fails on any leftover .credentials-*.tmp.
- TestFileBackend_ConcurrentWritersSerialize runs two goroutines
  writing distinct keys; both records must survive (verifies the
  flock around load-mutate-save).
- TestChain_SetWarnsOnFallback captures ChainStderr via bytes.Buffer
  and asserts the one-line warning when a writable backend errors
  before the file backend succeeds.
2026-05-27 08:21:57 +00:00
Tink bot f04930137e test(veans): pin runUpdate's call-order invariants
The two ordering rules in commands/update.go::runUpdate aren't enforced
by anything beyond the lines being written in that sequence:

  1. MoveTaskToBucket runs AFTER UpdateTask, so a status transition
     doesn't clobber freshly attached labels.
  2. The scrapped-reason comment posts BEFORE the bucket move, so the
     audit trail reads chronologically.

Both are documented in CLAUDE.md but neither is exercised by the e2e
suite: TestUpdate_DescriptionReplaceUniqueness is the only update-side
e2e and it only covers --description-replace-old/new.

Add two unit tests that drive runUpdate against an httptest.Server and
assert the exact (method, path) sequence. Sanity-checked locally by
swapping the field-update and bucket-move blocks — both tests fail with
a clear order diff, confirming they catch the regression that's most
likely to slip through review.
2026-05-27 08:21:57 +00:00
Tink bot ba6615f378 feat(veans): warn when Chain.Set falls back past a failed backend
A keyring transient failure on Set silently falls through to the file
backend today, which leaves a stale keyring entry from any prior
successful write shadowing the new file-backend token. Fixing the
shadow itself is deferred (would need a Set-and-Delete coordination,
or a stricter contract).

What we can do cheaply: surface the fallback so an operator hitting
the shadow has a breadcrumb. On Chain.Set fallthrough past a writable
backend that errored, print:

  veans: credential store: keyring rejected write (X); falling back to file

The warning goes to stderr (not the structured envelope — Set still
returns nil because the write landed somewhere). Env-backend's
read-only skip is unchanged and silent.

ChainStderr is exposed as a package var so tests can capture/assert
the warning when we backfill credential-store coverage.
2026-05-27 08:21:57 +00:00
Tink bot 75e546f0c1 feat(veans): make the HTTP client timeout configurable via .veans.yml
The 30s timeout on the client.New HTTPClient was hard-coded and
opaque. Long-running paginated reads against slow networks were
tripping it with no escape hatch.

Lift the value into a named constant and let .veans.yml override it
via a new optional http_timeout field (Go duration syntax, e.g.
"60s", "5m"). The field has omitempty so a freshly-written
.veans.yml from `veans init` doesn't surface the knob — operators
who need to tune it can hand-edit, but it stays out of the way for
the common case.

Runtime loader applies the override after client.New if set;
bootstrap- and login-time clients (built before .veans.yml exists)
keep the default.
2026-05-27 08:21:57 +00:00
Tink bot c4a0575305 feat(veans): offer "create a new project" from init's picker
The project picker used to require at least one pre-existing project
and would otherwise hard-error: "no projects visible to this user —
create one in the Vikunja UI first". Now it always offers an extra
numbered entry "Create a new project" and, when the user picks it,
prompts for a title (required) + identifier (optional). Empty-list
case routes straight to creation.

Backed by a new client.CreateProject(ctx, *Project) method (`PUT
/projects`); the e2e harness now uses that instead of the raw c.Do
call it did before.

Also fixed a latent bufio bug in StdPrompter.ReadLine that this work
surfaced: every call created a fresh bufio.Reader, which read-ahead a
buffer and threw it away on return. Second+ prompts read empty. Reuse
one buffered reader on the StdPrompter instance.
2026-05-27 08:21:57 +00:00
Tink bot 9b8ad4d027 feat(veans): URL discovery on init, port of the frontend's heuristic
The previous init flow took whatever the user typed for --server and
called GET <url>/api/v1/info on it. If the user typed
"vikunja.example.com" (no scheme), or pasted the URL with /api/v1 in
it (double-suffix), or pointed at a localhost install on the default
:3456 port without typing the port, we'd hand back a raw HTTP error.

New `client.DiscoverServer` ports the frontend's
helpers/checkAndSetApiUrl.ts discovery: probe a small ordered set of
plausible bases for /api/v1/info, return the first one that returns
parseable Info. Candidate order:

  1. scheme://host[:port]/path           (as the user typed it)
  2. scheme://host:3456/path             (default API port)
  3. opposite scheme of (1)
  4. opposite scheme of (2)

Heuristics:
- Missing scheme → https for public hosts, http for localhost /
  127.0.0.1 / [::1] (matches most CLIs' behaviour)
- Trailing /api/v1 from a pasted URL is stripped before probing, so
  we don't double up to /api/v1/api/v1/info
- Trailing slashes normalized

Errors now list everything we tried + the last underlying network
error, so the user can see why a URL failed instead of just
"GET /info: connection refused":

  veans: VALIDATION_ERROR: couldn't find a Vikunja instance reachable
    from "vikunja.example.com" — tried:
    - https://vikunja.example.com/api/v1/info
    - https://vikunja.example.com:3456/api/v1/info
    - http://vikunja.example.com/api/v1/info
    - http://vikunja.example.com:3456/api/v1/info
    last error: dial tcp: lookup vikunja.example.com: no such host

bootstrap.Init now defers URL canonicalisation to DiscoverServer and
caches the matched info from the probe (no second /info round-trip).

Unit tests cover the candidate-builder across the common shapes:
bare hostname, localhost, /api/v1-suffixed paste, explicit port,
subpath install, 127.0.0.1:3456, trailing slash. e2e green.
2026-05-27 08:21:57 +00:00
Tink bot 814b2a635f feat(veans): install agent hooks during init instead of just printing
Adds a final step to bootstrap.Init that offers to wire `veans prime`
into Claude Code and OpenCode automatically. Per-agent yes/no prompts
default to "yes" for Claude Code and "no" for OpenCode; --install-claude
/ --install-opencode flags skip the prompt for scripted contexts;
--no-hooks falls back to the previous behaviour of just printing the
snippets.

Claude Code:
  - Writes/merges .claude/settings.json
  - JSON merge preserves existing keys (model, permissions, other hooks)
    and only appends a `veans prime` command entry under SessionStart
    and PreCompact if one isn't already there
  - Idempotent: re-running reports "Already configured" without
    duplicating entries

OpenCode:
  - Writes .opencode/plugin/veans-prime.ts with the standard handler
    skeleton
  - Existing files are left alone (no TS-merge story for v0)

Failures during hook install are non-fatal: the repo is already
configured, so the user gets a warning + the printed snippets as a
fallback path.

Unit tests cover the merge logic (fresh file, idempotent rerun,
preserving user's other hooks/keys), the install actions
("Wrote"/"Updated"/"Already configured"), and the offer flow
(flags-bypass-prompt vs prompt-when-unset vs no-hooks).
2026-05-27 08:21:57 +00:00
Tink bot 1bc3afa430 feat(veans): match existing bucket titles via case-insensitive alias table 2026-05-27 08:21:57 +00:00
Tink bot 4ac89741e3 feat(veans): reuse owned bot or prompt for fresh name on collision 2026-05-27 08:21:57 +00:00
Tink bot cd7cc113a1 docs(veans): AGENTS.md cheat sheet for coding agents
Captures the non-obvious things an agent will hit working on this
submodule:

- Wire-format quirks (view_kind/bucket_configuration_mode are JSON
  strings; Task.BucketID is always 0 in GET — use ?expand=buckets and
  CurrentBucketID; POST /tasks doesn't move buckets, use the dedicated
  bucket-tasks endpoint; bot creation is at /user/bots; APIToken
  expires_at is required, use FarFuture for "no expiry").
- Permission discovery via /routes (group names are path-derived; use
  PermissionsForBot at runtime instead of hard-coding).
- OAuth shape (PKCE/S256 mandatory, no client registration, JSON-only
  token exchange, loopback redirect via 127.0.0.1:0, Shutdown uses
  context.WithoutCancel to drain on outer cancel).
- Credential chain order + per-test HOME/XDG override.
- Identifier validation (runelength only) + base-36 timestamp suffix
  trick for unique e2e identifiers.
- mage Aliases map (without it, `mage test` rejects the namespace).
- License-header enforcement via local .golangci.yml + code-header-
  template.txt copy.
- Things to actively avoid: bare exec.Command, committing the built
  binary, stdout from `prime` outside a configured workspace.

CLAUDE.md is a symlink to AGENTS.md so Claude Code picks it up via
either name.
2026-05-27 08:21:57 +00:00
Tink bot 632579b304 ci(veans): add fast veans-test job for unit tests 2026-05-27 08:21:57 +00:00
Tink bot c1d5272afe feat(veans): switch OAuth handshake to a loopback HTTP server 2026-05-27 08:21:57 +00:00
Tink bot b18762171d feat(veans): add browser launcher helpers for OAuth flow 2026-05-27 08:21:57 +00:00
Tink bot 952ad89a8b chore(veans): apply veans golangci pass across sources 2026-05-27 08:21:57 +00:00
Tink bot 8ef796f016 chore(veans): add veans-local golangci config 2026-05-27 08:21:57 +00:00
Tink bot 35aa486eb5 feat(veans): use OAuth 2.0 Authorization Code + PKCE as default auth
Vikunja's built-in OAuth server (Vikunja 2.3+) does not require client
registration and accepts arbitrary client_ids — it just enforces PKCE
(S256) and constrains redirect URIs to the vikunja- scheme. Earlier I
deferred OAuth on the assumption it needed a registered client; that
was wrong, and the docs make the path much smoother than POST /login.

The custom-scheme constraint (no http:// loopback) is side-stepped by
manual paste-back: veans prints the authorize URL, the user signs in,
their browser fails to open vikunja-veans-cli://callback?code=... and
shows an error, the user copies the URL from the address bar and
pastes it back. CLI extracts code + state, verifies state for CSRF,
exchanges via POST /api/v1/oauth/token (JSON body — Vikunja rejects
form-encoded), and returns the access token.

Resolution order in AcquireHumanToken:
  1. --token (paste-in JWT or personal API token; SSO/OIDC users)
  2. --use-password / --username + --password (POST /login)
  3. OAuth flow (interactive default)

login command supports the same --use-password / --token escape hatches
for token rotation on instances with OAuth disabled.

Includes unit tests for the PKCE generator (verifier shape per RFC 7636,
challenge = SHA256(verifier) base64url-no-pad), authorize-URL
construction, and the lenient callback parser (full URL / query-only /
bare code).
2026-05-27 08:21:57 +00:00
Tink bot d0c77ad6fe docs(veans): add README with quick-start guide 2026-05-27 08:21:57 +00:00
Tink bot 4c3d449a35 test(veans): add e2e suite covering init, tasks, claim, prime flows 2026-05-27 08:21:57 +00:00
Tink bot 3a7bcb2a50 chore(veans): gitignore built binary 2026-05-27 08:21:57 +00:00
Tink bot df7a60d137 feat(veans): add login command for token rotation 2026-05-27 08:21:57 +00:00
Tink bot 2e2393121b feat(veans): add api passthrough command 2026-05-27 08:21:57 +00:00
Tink bot e8cdfcf023 feat(veans): add prime command for agent prompt injection 2026-05-27 08:21:57 +00:00
Tink bot b9551d55ba feat(veans): add claim command for assigning and bucket transition 2026-05-27 08:21:57 +00:00
Tink bot 6ebe25bfbc feat(veans): add update command with description and status transitions 2026-05-27 08:21:57 +00:00
Tink bot 6b756d92c3 feat(veans): add create command with labels and relations 2026-05-27 08:21:57 +00:00
Tink bot 2425d9923e feat(veans): add label get-or-create helper 2026-05-27 08:21:57 +00:00
Tink bot e88427ca3c feat(veans): add show command with PROJ-NN/#NN ID resolver 2026-05-27 08:21:57 +00:00
Tink bot 5e80c17281 feat(veans): add list command with filters and JSON output 2026-05-27 08:21:57 +00:00
Tink bot 081373bb48 feat(veans): add shared command runtime and git branch helper 2026-05-27 08:21:57 +00:00
Tink bot 81f4845a6b feat(veans): wire init cobra command 2026-05-27 08:21:57 +00:00
Tink bot 37b6ff538b feat(veans): orchestrate init bootstrap from probe to config write 2026-05-27 08:21:57 +00:00
Tink bot d2c3f3244d feat(veans): discover /routes for permission-group negotiation 2026-05-27 08:21:57 +00:00
Tink bot 1f5abaa6fb feat(veans): require APIToken.ExpiresAt with FarFuture sentinel 2026-05-27 08:21:57 +00:00
Tink bot 6b48a37710 feat(veans): add canonical status to bucket-title mapping 2026-05-27 08:21:57 +00:00
Tink bot 36fb0f0ace feat(veans): add .veans.yml schema and config helpers 2026-05-27 08:21:57 +00:00
Tink bot 878233f758 feat(veans): add transient human auth flow 2026-05-27 08:21:57 +00:00