Commit Graph

14012 Commits

Author SHA1 Message Date
kolaente 8a4f5cbe11 fix(models): make API tokens work on /api/v2 routes
Sub-phase G validation caught that a token scoped to e.g.
`labels.read_one` was rejected on /api/v2/labels because the route
collector only stripped /api/v1/ from paths and did not know about
v2's REST-style verbs (POST create, PUT/PATCH update, inverted
from v1 where PUT creates and POST updates).

Introduce a shadow apiTokenRoutesV2 map keyed under the same
(group, permission) names as the v1 entries. Route collection now
routes v2 paths into this shadow map and CanDoAPIRoute consults
both tables, so the same permission bit authorizes the v1 and v2
endpoints for the same resource without changing the data shape
served at /api/v1/routes (which the frontend token UI depends on).

Also teach getRouteDetail about PATCH so Huma's AutoPatch-synthesized
PATCH routes collapse to the `update` permission instead of being
dropped.
2026-05-31 12:56:57 +00:00
kolaente 15d8ac5f49 feat(auth): add GetAuthFromContext for Huma handlers 2026-05-31 12:56:57 +00:00
kolaente 47b2d1043b chore(deps): add huma/v2 and align transitive deps
Promotes huma/v2 to a direct dep (now imported by pkg/routes/api/v2 and
pkg/modules/humaecho5) and bumps clipperhouse/displaywidth to v0.11.0,
which is required for compatibility with uax29/v2 v2.7.0 that huma pulls
in. Other version bumps are go-mod-tidy consequences of MVS.
2026-05-31 12:56:57 +00:00
kolaente 5fefa88577 feat(routes): scaffold /api/v2 Echo group 2026-05-31 12:56:57 +00:00
kolaente 5fa6d66c41 feat: vendor humaecho adapter for echo/v5 2026-05-31 12:56:57 +00:00
kolaente 137f31bb20 fix(docker): make /tmp world-writable so exports work under any UID
The scratch image shipped /tmp owned by 1000:1000 and writable only by
UID 1000, so containers run under a different user (e.g. Unraid's
99:100, OpenShift random UIDs, or any `user:` override) could not create
the temp file used for data exports, failing with:

  error creating temp file: open /tmp/vikunja-export-*.zip: permission denied

The builder-stage `chmod 1777 /tmp` did not survive into the final image
(see #2316, which had to add --chown to make it writable for UID 1000),
so the world-writable intent was lost. Force the mode at copy time with
BuildKit's --chmod=1777, restoring a normal sticky, world-writable /tmp
that works for every UID.

Closes go-vikunja/vikunja#2755
2026-05-30 14:21:22 +00:00
kolaente e31d73b3df fix(keyvalue): treat undecodable cached values as a cache miss
A GetWithValue deserialization error in RememberFor was returned as fatal.
On a Redis upgrade the metrics counters live under the same keys as before
but were stored as plain int64, so the first decode into the new envelope
would fail and the metric would break permanently. Treat such errors as a
miss and recompute/overwrite so the cache self-heals.
2026-05-30 13:48:01 +00:00
kolaente 9a810f7632 refactor(user): remove the now-empty listeners file
The user package no longer registers any event listeners, so drop the
empty RegisterListeners hook and its caller.
2026-05-30 13:48:01 +00:00
kolaente 71dcb096be test(metrics): verify counts are read from the right table 2026-05-30 13:48:01 +00:00
kolaente 054050b1e2 test(keyvalue): cover RememberFor TTL caching 2026-05-30 13:48:01 +00:00
kolaente 0248bdf5e7 feat(metrics): invalidate the user count cache on registration
Registration is the one hot path where instant freshness is worth an
extra COUNT(*), so bust the cache there rather than waiting for the TTL.
2026-05-30 13:48:01 +00:00
kolaente 9e3e884dac refactor(metrics): drop inline file count tracking
The file count is now read from the database on demand.
2026-05-30 13:48:01 +00:00
kolaente 72a231620d refactor(metrics): drop the project/task/team/attachment count listeners
These counts are now read from the database on demand. The events
themselves stay - they are still used by webhooks and notifications.
2026-05-30 13:48:01 +00:00
kolaente 06000b7a03 refactor(metrics): drop the user count listener
The user count is now counted on demand, so the increment-on-create
listener is no longer needed.
2026-05-30 13:48:01 +00:00
kolaente 051f734f3d refactor(metrics): count entities on demand with a TTL cache
Instead of priming a counter at startup and keeping it in sync via events,
each entity count is now read directly from the database and cached for
30s (countCacheTTL). The cache is the correctness guarantee: counts are at
most one TTL stale and self-healing, so they can never permanently drift.

This fixes vikunja_user_count never updating after registration (#2650):
the count no longer depends on every mutation path dispatching an event.
2026-05-30 13:48:01 +00:00
kolaente ec2f154e10 feat(keyvalue): add RememberFor for TTL-cached values 2026-05-30 13:48:01 +00:00
kolaente 1af6d7763b fix(desktop): show tray icon in packaged builds
The tray icon was loaded from desktop/build/icon.png, but build/ is
electron-builder's default buildResources directory, whose contents are
not packaged into the app. The icon therefore existed when running from
source but was missing in every released build, leaving an empty tray
icon.

Load the icon from the packaged app root instead and add icon.png there,
rendered from the circular logo.svg so it has transparent corners rather
than the square full-bleed source artwork.

Fixes #2668
2026-05-30 13:30:55 +00:00
renovate[bot] e0fa2bbed4 chore(deps): update dependency vue-tsc to v3.3.3 2026-05-30 13:17:09 +00:00
kolaente 9456223556 fix: prevent package postinstall hang when generating jwt secret
The postinstall scripts generated the jwt secret with:

  cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1

This relies on SIGPIPE to terminate the infinite `cat /dev/urandom`
once `head` has read its single line. Inside a dpkg/apt maintainer-script
context the SIGPIPE disposition is not reliably delivered, so
`cat /dev/urandom` spins forever, the postinstall never returns, and the
whole `dpkg -i` / upgrade hangs.

Read a bounded 512 bytes with `head -c` instead so nothing depends on
SIGPIPE to terminate. 512 random bytes yield ~124 alphanumerics on
average, so the trailing `head -c 32` reliably produces a full 32-char
secret while staying dependency-free.

Fixes #2660
2026-05-30 12:39:49 +00:00
dependabot[bot] f7921238e6 chore(deps): bump axios from 1.15.2 to 1.16.0 in /frontend
Bumps [axios](https://github.com/axios/axios) from 1.15.2 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.2...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 08:48:43 +00:00
Rémi Lapeyre 069685f2a7
fix(caldav): return 404 when trying to access a project that cannot exist with CalDAV (#2796) 2026-05-28 08:14:52 +02:00
Frederick [Bot] 50bece8cdb chore(i18n): update translations via Crowdin 2026-05-28 02:06:56 +00:00
renovate[bot] 7d1372ece3 chore(deps): update dev-dependencies 2026-05-27 21:18:08 +00:00
kolaente 2395239f0b
fix(ci): generate config.yml.sample in release-os-package for vikunja
vikunja's nfpm.yaml packs ./config.yml.sample as /etc/vikunja/config.yml.
The release-binaries action already regenerates it for the zip bundles,
but release-os-package runs on a fresh runner without that file, so
nfpm aborted with "matching ./config.yml.sample: file does not exist"
on every vikunja os-package matrix shard (the veans shards skip this
step entirely).

Add a vikunja-only step to regenerate it before nfpm runs.
2026-05-27 18:16:38 +02:00
kolaente 1e1fcaafbc
fix(ci): drop "./" from PACKAGE_OUTPUT_DIR so strip-path-prefix matches
The s3-action expands the upload glob into paths without a leading
"./", but strip-path-prefix was set to "./dist/os-packages/" (or
"./veans/dist/os-packages/"). The prefix never matched, so packages
landed at /<project>/<version>/<project>/dist/os-packages/<file>
instead of /<project>/<version>/<file>.

Drop the "./" prefix to match the working DIST_PREFIX pattern in
release-binaries.
2026-05-27 17:35:58 +02:00
kolaente 4f8a44de89
fix(ci): switch release composite actions to unstable on non-tag builds
The release-binaries and release-os-package composite actions were
comparing the raw release-version input against the literal "main" to
decide whether to use "unstable" for filenames and the S3 directory.
Callers always pass `steps.ghd.outputs.describe`, which is a value
like `v2.3.0-408-ge053d317` on non-tag builds — so the check never
fired and unstable artifacts landed under `/<project>/<describe>/`
with `<project>-<describe>-...` filenames.

Drive the switch from `github.ref_type == 'tag'` instead, matching the
pattern the desktop and config-yaml jobs in release.yml already use.
The raw describe value still flows into RELEASE_VERSION so the binary
and package metadata keep the precise commit reference.
2026-05-27 17:32:47 +02:00
kolaente e053d3172f
fix(ci): escape ${{ secrets.* }} mention in release-binaries description
GitHub's action manifest parser evaluates `${{ ... }}` expressions inside
`description:` block scalars, and `secrets` isn't a valid context in a
composite action — so the literal example text in the docstring caused
manifest validation to fail before any step ran.
2026-05-27 17:02:56 +02:00
kolaente be7eabb9b3 ci: move build-mage prep job out of test.yml into release.yml
build_mage_bin is only consumed by publish-repos in release.yml, so it
doesn't belong in the test workflow. Move it to release.yml as a
prep job and add it to publish-repos's needs list.
2026-05-27 13:01:44 +00:00
kolaente ed9df9064c refactor(ci): derive composite-action inputs from project name
Reviewer asked us to stop over-configuring the release-binaries and
release-os-package composite actions — they're called only with
vikunja or veans, so per-project paths, artifact names, cache keys, S3
target, and version-or-unstable can all be derived inside the action
from the project name. The xgo-out-name input goes away too.

Vikunja-specific pre-build (downloading frontend_dist, generating
config.yml.sample) now happens inside the action, gated on the project
input. Callers no longer need those preamble steps.

Secrets stay as inputs — composite actions can't read \`\${{ secrets.* }}\`
directly; passing them through is the simplest workaround.

Each callsite shrinks to ~13 lines of mostly-secret pass-through plus
2-4 lines of real parameters.
2026-05-27 13:01:44 +00: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 e903b72b9e refactor(ci): call release composite actions from release.yml
Replace the inline bodies of binaries, veans-binaries, os-package, and
veans-os-package jobs with calls to the new release-binaries and
release-os-package composite actions. Each call site is now ~25 lines
of inputs instead of ~75 lines of duplicated mage+upx+gpg+s3 plumbing.

publish-repos switches from the parent's ./mage-static to the
prebuilt build_mage_bin artifact so it can drive build/'s repo metadata
targets inside the publish-repos containers.
2026-05-27 13:01:44 +00:00
kolaente 95f65c4712 ci: prebuild static build/mage for repo metadata containers
publish-repos runs inside ubuntu/fedora/archlinux containers that don't
ship a Go toolchain. Compile build/magefile.go into a static binary in
the test workflow (mirroring the existing mage_bin job for the parent
magefile) and upload it as the build_mage_bin artifact so publish-repos
can chmod+x and run it without setup-go.
2026-05-27 13:01:44 +00:00
kolaente c690f74d75 feat(ci): add release-binaries and release-os-package composite actions
Two reusable composite actions wrap the CI side of the release pipeline:

- release-binaries: setup-go, install mage + upx, cache xgo, invoke
  `mage release:build <project>` from build/, GPG-sign the zip bundles,
  upload to S3, store binaries and zips as workflow artifacts.

- release-os-package: download a binaries artifact, install mage,
  `mage release:prepare-nfpm-config <project> <arch>`, stage the binary,
  nfpm pack (with rpm signing inline and archlinux signing after), upload
  to S3, store the package as an artifact.

Both actions are parameterized on project name, output paths, artifact
names, S3 target, and GPG/S3 secrets — adding a third Go binary to the
monorepo just means defining its project in build/magefile.go and adding
a four-line call site in release.yml.
2026-05-27 13:01:44 +00:00
kolaente 8313d032ea feat(build): add centralized release magefile module
New build/ Go module hosts the full release pipeline (xgo cross-compile,
upx, sha256, zip bundles, nfpm templating, deb/rpm/apk repo metadata)
for every Go binary in the monorepo. Parametric on project name —
`mage release:build vikunja` and `mage release:build veans` both flow
through the same code.

The module is intentionally self-contained: it depends on nothing but
stdlib + mage, and duplicates the small filesystem helpers (copyFile,
moveFile, sha256File) rather than importing them from a project
magefile. That keeps the release tooling free to evolve without
touching project code.
2026-05-27 13:01:44 +00:00
kolaente f39cf00290 ci: register custom actionlint runner labels
Declare namespace-profile-default and blacksmith-8vcpu-ubuntu-2204 as
known self-hosted runners so actionlint stops flagging them as unknown.
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
kolaente d5ab54941f
fix(deps): bump ip-address to >=10.1.1 in desktop workspace
Resolves a medium-severity XSS in Address6 HTML-emitting methods
(GHSA / Dependabot alert #224). Vulnerable range: <=10.1.0,
patched in 10.1.1. The package is pulled in transitively through
socks -> socks-proxy-agent in the Electron build chain
(devDependency only), but we add a pnpm override to ensure the
patched version is used everywhere. The frontend workspace already
has the equivalent override.
2026-05-27 11:10:24 +02:00
kolaente e08f05119d
fix(deps): bump qs to 6.15.2 in desktop
Resolves Dependabot alert #233: qs.stringify crashes with TypeError on
null/undefined entries in comma-format arrays when encodeValuesOnly is
set (DoS, medium severity).

Updates transitive dependency via pnpm update from 6.15.0 to 6.15.2.
2026-05-27 11:09:27 +02:00
kolaente 7be5026113
fix(deps): bump tmp to >=0.2.6 to fix path traversal vulnerability
Adds a pnpm override for `tmp` in both the `frontend` and `desktop`
workspaces to force the patched version (0.2.6). The previous transitive
resolutions (`tmp@0.0.33` via external-editor in frontend, `tmp@0.2.3`
via tmp-promise in desktop) are vulnerable to a path traversal via
unsanitized prefix/postfix that enables directory escape.

Addresses Dependabot alerts #234 (desktop) and #235 (frontend).
2026-05-27 11:09:20 +02: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