-
Notifications
You must be signed in to change notification settings - Fork 0
Web Server Integration
There are two different things "the git-task web server" can mean, and it's worth separating them before writing any code:
-
The bundled UI (
git task web): a companion server this CLI can install and manage for you, giving you a browser-based view of your tasks with no code of your own. -
Your own integration: any service you build, HTTP or otherwise, that drives git-task by
calling it with
--format jsonand parsing the result. This is how you'd expose task data to a dashboard, a Slack bot, a CI check, or anything else with a JSON contract you control.
This page covers both, and then goes deep on the second, since that's the one you write yourself.
git-task ships an optional companion, git-task-web, a small Node.js server distributed
separately on npm, that this CLI can install and manage as a background process. git task web
doesn't implement that server itself: it wraps npm install, spawns the installed server as a
detached child process, and tracks whether it's still running.
git task web start # installs git-task-web first if needed, prompts to confirm
git task web start --yes # non-interactive: install without prompting
git task web start --port 4700 --host 0.0.0.0
git task web status
git task web stopRequirements: Node.js 20 or newer, and npm, both on PATH. The package installs under
~/.local/share/git-task/web (override the base directory with GIT_TASK_DATA_DIR), isolated
from any project's own node_modules. Default bind address is 127.0.0.1:4600.
Behavior worth knowing:
- The server survives the terminal that started it closing. It runs in its own session (
setsidon Unix), not just detached, so a closed terminal or aSIGHUPdoesn't take it down the way it would a plain background job. -
web startwaits up to 10 seconds for the port to accept a connection before reporting success; a slow-starting server is still spawned, just reported as not-yet-ready. - Combined stdout and stderr of the server process is appended to
~/.local/share/git-task/web.log(web statusprints the exact path). Unlike git-task's own fully-silentauto-syncbackground worker, this log is kept, since the server is long-lived and user-visible. -
web stopsendsSIGTERM, waits up to 5 seconds, then escalates toSIGKILLif the process is still alive. -
--format jsonon anywebsubcommand returns{ running, pid, url, log }.
What git-task-web itself exposes as an HTTP API, and in what shape, is defined by that package,
not by this CLI. This page documents what git task web (the process manager) does. If you want
programmatic access to task data over HTTP with a contract you control, keep reading.
There is no long-running API server built into the git-task binary itself. Every
--format json invocation is a fresh process: it opens the repo, does the work, prints one JSON
document, and exits. That's a deliberate, simple integration point: any language that can spawn a
subprocess and read stdout can drive git-task, without linking against it or speaking a custom
protocol.
- Spawn
git task(or thegtaskbinary directly) with argv built as an array, never a shell string. - Always include
--format json. - Read all of stdout and parse it as one JSON document.
- Branch on
ok, not the process exit code. A validation or not-found error is exit code 1 and a well-formed JSON document on stdout; that's the expected path, not a crash. See JSON Output for the full envelope and error-kind reference.
Pass arguments as an array (execFile, not exec; subprocess.run([...]), not
os.system(...)) so a task title containing spaces, quotes, or shell metacharacters can never be
interpreted as part of the command. This isn't a git-task-specific precaution; it's the same rule
for shelling out to anything with values a user typed.
// wrong: a title of `"; rm -rf ~ #` becomes part of the shell command
exec(`git task new "${title}" --format json`);
// right: title is always one argv element, never parsed by a shell
execFile('git', ['task', 'new', title, '--desc', description, '--format', 'json']);// git-task-client.js
const { execFile } = require('node:child_process');
function runGitTask(args, cwd) {
return new Promise((resolve, reject) => {
execFile(
'git',
['task', ...args, '--format', 'json'],
{ cwd, maxBuffer: 10 * 1024 * 1024 },
(_err, stdout, stderr) => {
// A non-zero exit is expected for `ok: false` responses; don't reject on `_err` alone.
try {
resolve(JSON.parse(stdout));
} catch {
reject(new Error(`git-task produced no JSON on stdout: ${stderr || stdout}`));
}
}
);
});
}
module.exports = { runGitTask };// server.js
const express = require('express');
const { runGitTask } = require('./git-task-client');
const app = express();
app.use(express.json());
const REPO_PATH = '/path/to/your/repo';
app.get('/api/tasks', async (req, res) => {
const args = ['ls'];
if (req.query.status) args.push('--status', req.query.status);
if (req.query.kind) args.push('--kind', req.query.kind);
const result = await runGitTask(args, REPO_PATH);
if (!result.ok) return res.status(422).json(result.error);
res.json(result.data);
});
app.get('/api/tasks/:id', async (req, res) => {
const result = await runGitTask(['show', req.params.id], REPO_PATH);
if (!result.ok) {
const status = result.error.kind === 'not_found' ? 404 : 422;
return res.status(status).json(result.error);
}
res.json(result.data);
});
app.post('/api/tasks', async (req, res) => {
const { title, kind, description, priority, assignee } = req.body;
const args = ['new', title, '--desc', description ?? ''];
if (kind) args.push('--kind', kind);
if (priority) args.push('--priority', priority);
if (assignee) args.push('--assignee', assignee);
const result = await runGitTask(args, REPO_PATH);
if (!result.ok) return res.status(422).json(result.error);
res.status(201).json(result.data.task);
});
app.post('/api/tasks/:id/status', async (req, res) => {
const result = await runGitTask(['status', req.params.id, req.body.status], REPO_PATH);
if (!result.ok) return res.status(422).json(result.error);
res.json(result.data.task);
});
app.listen(3000);Every field in a request body above ends up as its own argv element, so there's no escaping to get
wrong. The title/status values can contain anything, including characters that would matter to
a shell, and it makes no difference here.
Two processes can race to append to the same task: two web requests editing the same task at once,
or a request racing an interactive git task edit someone's running locally. git-task detects this
itself, via compare-and-swap on the underlying git ref, and retries once internally before
surfacing error.kind: "conflict" if both attempts lost the race. At the scale a single-repo
integration usually sees, that's already enough. If you expect real contention (many concurrent
writers touching the same tasks), wrap the call in a small retry loop and re-fetch the task with
show before reapplying, rather than assuming your view of it is still current:
async function runGitTaskWithRetry(args, cwd, attempts = 3) {
let last;
for (let i = 0; i < attempts; i++) {
last = await runGitTask(args, cwd);
if (last.ok || last.error?.kind !== 'conflict') return last;
}
return last;
}- Spawning
githas real, if small, overhead: repo discovery, opening the object database. For a UI that lists tasks on every keystroke of a search box, filter server-side withls's own flags (--status,--label,--kind, and so on) rather than shelling out once per keystroke and filtering the results yourself in application code. -
lsdefaults to just the current repo. If you've registered multiple repos (see Sync and Multi-Repo) and don't want the aggregate, pass--hereexplicitly rather than relying on the default, since the default behavior depends on whether a repo happens to be registered at all. -
history/--with-historyroughly doubles response size for a repo with long-lived tasks. Leave it off unless the UI actually renders an audit trail.
git-task itself has no concept of a user, a session, or a permission check beyond what your operating system's filesystem permissions already enforce on the repo. Anything you build in front of it, a web server, a chat bot, a CI job, is responsible for its own authentication and authorization. Don't expose an endpoint that runs arbitrary git-task subcommands built from unvalidated request input; constrain which subcommands and flags a given route can trigger, the same way you would for any other internal command wrapped by a public API.
Related: JSON Output for the full response contract this section builds on.
git-task
Start here
Using tasks
Building on git-task
Contributing