2026-06-16 12:56:23 +02:00
2026-08-12 19:12:02 +02:00
2026-06-12 11:36:45 +02:00
2026-09-21 14:18:32 +02:00
2026-09-21 14:18:32 +02:00
2026-09-06 23:45:46 +02:00
2026-09-06 23:45:46 +02:00
2026-09-06 23:45:46 +02:00
2026-05-20 11:38:19 +02:00
2026-09-06 23:45:46 +02:00
2026-09-20 14:13:10 +02:00
2026-09-06 23:45:46 +02:00
2026-09-06 23:45:46 +02:00
2026-09-06 23:45:46 +02:00

vikunja-taskman

A desktop task manager that puts Vikunja and Jira tasks in one native window, plus taskman, a CLI over the same Vikunja instance.

Two binaries are built from this repo:

what built with
app (repo root) Wails v2 desktop app — Go backend, React frontend in WebView2 wails build
taskman (cmd/taskman) cobra CLI for Vikunja go install ./cmd/taskman

They share internal/vikunja, a Go client for the Vikunja REST API. The app currently still talks to Vikunja through its own TypeScript driver (frontend/src/lib/drivers/vikunja.ts) rather than that client; unifying them is not done.

Attachments are shared for real, though: both sides keep a task's files in the same S3 bucket under a prefix named after the task id, so a file attached from the CLI shows up in the app.

The desktop app

Hierarchical task tree, task detail pane, Vikunja stage labels, Jira workflow transitions and worklogs, S3 per-task attachments, Typesense full-text search, agenda panel.

wails dev      # live development
wails build    # or task

It is a desktop app and does not run in a browser — all frontend/backend traffic goes over the Wails Go bridge, which is also how it avoids CORS. See AGENTS.md for the details.

The CLI

go install ./cmd/taskman

Configuration comes from the environment (flags override it):

export VIKUNJA_TASKMAN_URL=https://vikunja.example.com/api/v1   # must include /api/v1
export VIKUNJA_TASKMAN_TOKEN=tk_...
export VIKUNJA_TASKMAN_PROJECT=1                                # default project
export VIKUNJA_TASKMAN_BUCKET=vikunja-docs                      # attachment bucket

Attachments additionally need RUSTFS_ENDPOINT, RUSTFS_ACCESS_KEY and RUSTFS_SECRET_KEY; the S3 layer is rustfs-cli's, which reads those itself.

The token is read from the environment rather than committed, because this repo is public. A .env in the working directory or its parent is also read.

taskman projects                    # list projects and their ids
taskman open                        # open tasks, newest first, minus the hidden stages
taskman open -s Review              # only tasks with that label
taskman open -p 2 --all             # another project, including done
taskman open --under 259            # one task and everything beneath it
taskman find actioner               # search titles and descriptions
taskman show 419                    # one task in full, with comments

taskman new "title" -d @notes.md    # body from a file, or "-" for stdin
taskman new "title" --parent 259    # hang it off an epic
taskman new "epic" --subtasks @kids # and its children, one title per line
taskman unparent 513                # detach a task from whatever it hangs under
taskman move 513 2                  # put it in another project, subtree and all
taskman edit 419 --title "better title" -s Waiting
taskman note 419 -                  # comment from stdin
taskman label 419 Waiting
taskman done 419

taskman attachments 419             # list attached files
taskman attach 419 diagnostic.zip   # upload
taskman fetch 419 -C ./downloads    # download all of them

Subtasks render under their parent, indented, at whatever depth the relations go — open and find group the listing rather than printing it flat. A task whose parent is not in the same listing, because the parent is done or carries a hidden stage, shows at top level with (under #259) in front of its title. show lists a task's children with their titles, and done refuses to close a task that still has open children and names them; --force closes it anyway. open --under <task> scopes a listing to one subtree, and new --subtasks files a parent and its children in one invocation instead of five with ids copied between them.

parent <task> <parent> moves a task: a task is in one place in the tree, so whatever it hung under is detached first and printed. The relation endpoint itself appends rather than replaces, which is why the move is explicit here — a task left under two parents renders one hierarchy in the listing and a different one in the app, because both place it under a single parent.

unparent <task> [<parent>] is the inverse. Naming a parent detaches that one; naming none detaches the task from every parent it has.

move <task> <project> puts a task on another board, naming the project either by the number projects prints or by enough of its title to pick out one. Everything under the task moves with it: a listing and the app both scope to a single project, so a parent left behind would read as childless there while its children read as roots in the project they landed in.

-o json works on every command. A task has one number: the id its URL ends with, which is what the API takes and what the desktop app renders and copies as #419. 419 and #419 are the same reference, and listings print it the way the app does.

Vikunja also keeps a per-project index and shows it in its own web UI. taskman does not speak it: it is a second small integer from the same range, and accepting both meant a number copied from one place acted on a different task in the other. It is in -o json as identifier for cross-referencing the web UI.

The stage labels are TODO (outstanding), Waiting (blocked on someone else), Review (done and only needing confirmation), Inconclusive (reported, investigated, not reproduced), Deferred (someday) and INFO (never was work — a note, a decision and why, a finding needing nothing done). taskman open hides Review, Inconclusive and INFO, since none of them is work anyone can pick up; -s <stage> and --all show them.

Bodies are light markup, converted to the HTML Vikunja stores - blank lines are paragraphs, fenced blocks are code, - is a list, and **bold**, backtick code and bare URLs are picked up. --html passes text through as-is. Reading from a file or stdin means never quoting a description through the shell.

Two deliberate omissions: there is no command that rewrites a description, and no delete. Descriptions are treated as append-only — additions go in a comment — and deletes in Vikunja are unrecoverable.

Notes worth knowing

  • Updating a task sends every column. POST /tasks/{id} is a replace, so a partial body zeroes whatever it omits — a bare {"done": true} blanks the description. Client.UpdateTask reads the task, merges, and writes it back whole; use it rather than posting fields directly.
  • per_page is capped at 50 by the server whatever you ask for. AllTasks pages for you.
  • Filters use Vikunja's syntax and run server-side before pagination, e.g. done = false, done = false && labels in 1, created > '2026-09-01'.

Both halves have a vet.sh: the one at the root covers the Go app and the CLI (gofmt, build, vet, tests, nag), and frontend/vet.sh covers the React app (nag, tsc). Each runs on its own; neither calls the other.

AGENTS.md is the long-form reference for both halves.

Description
No description provided
Readme 1.7 MiB
v3.32.0 Latest
2026-09-23 09:15:42 +00:00
Languages
Go 98.9%
Shell 1.1%