Overview
Deck is a single Go executable, deck. It has two modes that share the same code:
- CLI:
deck <command> [flags], for the terminal and for AI agents. - Panel:
deck ui, which starts an HTTP server on127.0.0.1:4004and opens the browser.
The panel screens are written in Next.js (React 19, Tailwind 4 and @thecodecodex/shell), but no Node server
runs: Next is only used at build time to generate a static export (HTML, CSS and JS). That export is embedded in
the binary with go:embed. At runtime, Go itself serves the pages and answers the API.
Components
| Piece | Where | Responsibility |
|---|---|---|
| CLI | cmd/deck, internal/commands/registry.go | Single command catalog: menu, --help, --schema and dispatch all come from it |
| Modules | internal/gitworkflow, internal/azure, internal/database, internal/mocks, internal/setup… | The logic of each domain |
| Configuration | internal/config | Reads and validates deck.yaml |
| Panel server | internal/hubserver | Embedded pages, security middleware and the /api/* routes |
| Screens | src/app, src/components | Static export; they only talk to same-origin /api/* |
| Public demo | src/demo, npm run build:demo | The same export with fictitious responses instead of the API |
| Updates | internal/selfupdate | deck update, channels, rollback and signature verification |
The path of a click
Moving an Azure Boards card from the panel:
Key points of this path:
- The client sends values, never flags. The body carries
idandstate; the server decides that they become--id 1234 --state "Code Review". See Security. - Each action is a child process of the binary itself. The handler does not reimplement business rules: it calls the same command you would run in the terminal. Panel and CLI never diverge.
- No shell. The process is created with arguments as an array;
;,$(...)or|in a field are just text.
The local API
All routes live in internal/hubserver, one per domain:
| Route | File | What it does |
|---|---|---|
/api/config | config.go | Non-secret part of deck.yaml for the screens |
/api/git | git.go | Multi-repo dashboard and batch actions |
/api/boards | boards.go | Card, features and the branch/PR workflow |
/api/github | github.go | PRs, my PRs, pipelines and workflow dispatch |
/api/cloud-logs, /api/cloud-logs/auth | logs.go, azure.go | Log search and Azure CLI device-code login |
/api/run-tests, /api/run-history | testes.go, historico.go | Playwright: list, run, abort, history |
/api/cobertura | cobertura.go | Report status, running tests with coverage and serving the HTML report |
/api/launcher, /api/mocks | servicos.go | Service ports, compose, mocks |
/api/database/* | database.go | Query catalog, execution and profiles (no passwords) |
/api/setup-env | setup.go | Diagnostics and installation steps |
/api/cli-release | release.go | Installed version, releases and binary downloads |
/api/skills | skills.go | The team’s agent skills |
Every route has a plain JSON contract. Input errors come back as HTTP 400 with a readable message; a missing external
binary (for example, gh not installed) comes back as 503.
Process execution
Every process goes through executar.go:
| Rule | Why |
|---|---|
| Arguments as an array, never a shell | Nothing the user types is interpreted as syntax |
stdin closed | A command that asks a question fails immediately instead of hanging |
| Own process group | Timeout and “Abort” kill the whole tree (including Playwright’s Chromium, the git called by gh…) |
| Per-action timeout | From 8 seconds (gh queries) to 60 minutes (E2E suite) |
| Output capped at 10 MB | A huge log does not bring the server down |
Long runs, such as the E2E suite, do not depend on the browser tab: closing the tab does not orphan Playwright, and the “Abort” button is how you stop it. Only one E2E run happens at a time.
Local state
deck ui has no database. The little state there is lives in local files, outside git:
| Data | Where |
|---|---|
E2E run history and the copied error-context.md files | <root>/.data/run-history.json and .data/run-history-artifacts/ |
| Queries created or edited in the panel | <root>/.data/queries-customizadas.json (the project catalog is never rewritten) |
| Team skills | <root>/.claude/skills/<id>/ |
| Secrets (passwords, tokens, PAT) | Secrets directory, outside the repository (deck secrets path) |
The CLI and the panel read and write the same files: deck tests history shows runs started from the panel.
Development mode
To work on the screens with hot reload, npm run dev starts two things:
next devon port 4005 (loopback only), serving the pages with hot reload;deck ui --dev http://127.0.0.1:4005on port 4004, which proxies the pages to Next and answers the API in Go.
You open http://localhost:4004 as usual. --dev only accepts an http loopback target with a port.