gift watches for the thing you care about — a GitHub push, the clipboard changing, a page that came back different, a file that was written — and answers it by running a script in a folder: build the site, pull the notes, restart the thing that just changed. One process watches all four, and around it sits a handful of git chores, run from the same command.
The oldest of the triggers is a GitHub push. That hook is a repository, the branches worth answering, a script, and the folder that script runs in. A delivery arrives and every hook watching that repository and that branch runs. Pick a push and watch it land:
The secret is checked before any of that: a delivery whose signature does not add up is refused. gift create can make the webhook on GitHub for you with gh, and then asks GitHub back whether it is really there — a hook in your file that GitHub never calls is worth being told about rather than assumed to work.
A hook is two halves: a trigger that says what has to happen, and a script to run when it does. gift triggers lists the four, gift create asks which one this hook is, and every question after that follows from the answer.
What was already there when the server started counts as nothing: restarting re-fires no hook. Each hook keeps its own books under logs/hooks/<name> — a line per request in hook.log, fired, quiet or failed, and an error.log that only exists once something has gone wrong, which is what makes one showing up worth noticing. A trigger is also just a folder: drop one into triggers/ with an index.js and it earns its place in gift create, config.json and gift list.
gift servegift restartgift stop · gift statusgift list · gift create · gift deletegift triggersgift loggift configgift updateThe port also carries a page of its own: the hooks as they are configured, and the last day of whatever happened — a delivery, a clipboard change, a page that came back different, a file that was written — newest first, with what each one's script printed, filling in while it is still printing it. Anything that looks like a credential is redacted before it is shown.
The same command runs a small set of git chores. gift help lists them, gift run opens a picker — a cursor to move, enter to run, a number key to run that row straight away — and gift <name> skips the picker. Enough of a name will do, and anything after it is passed on to the function itself.
Every git repository under one folder, in a table that keeps itself up to date: which branch each is on, whether anything is uncommitted, how many lines that is, and when it last moved. Pick some rows and the menu is what may be done to them — open one in an editor or an agent, read a diff, fetch, pull, push, branch, merge, rebase, stash, or commit and push the lot. A stash can be put back, a change discarded, a worktree branched off, a folder deleted outright, and / finds a repository in a folder too full to read. Nested checkouts and submodules get a row of their own.
gift repoA week of pull requests, grouped by the day they were opened. Weeks run Monday to Sunday, and it asks how many weeks back unless you have already said.
gift weeklygit pull --recurse-submodules --autostash in every repository below one folder, however deep it is. --dry-run says what it would pull without pulling it.
gift pullClone every repository owned by a GitHub organization or user into one folder. Public and private repositories, forks and archives are included unless you leave them out; clones run in parallel, existing folders can be pulled or skipped, and --dry-run shows the whole job first.
gift cloneBring down a whole repository, one folder, or one file from a GitHub URL without cloning or leaving a .git directory. It understands repository, tree, blob and raw links, and can read a chosen branch, tag or commit — including from a private repository when a token is available.
gift fetchEvery setting is in one file — config.json, beside the code. gift config opens it in your editor. Three kinds of section, and which one a setting belongs in follows from who reads it: gift's own are at the top level, each trigger's under triggers.<type>, each function's under functions.<name>:
{
"pm2_name": "gift",
"port": 3999,
"triggers": {
"github": { "github_webhook_secret": "…", "webhook_url": "…" },
"website": { "interval": 60000 }
},
"functions": {
"repo-master": { "repo_root": "/Users/me/projects" },
"weekly-prs": { "repos": "owner/repo1,owner/repo2", "author": "octocat" }
}
}It is written on first use with the settings worth looking at already in it, at their defaults, so opening it shows what there is to set rather than a blank page. Each one is declared in a config.schema.json next to the code that reads it — where its default, its description and the environment variable it reaches a script as all come from. A value already in the environment wins over the file, and a flag wins over both. The file is git-ignored and written 0600, because the webhook secret is in it — and hooks.json is treated the same way, because a website hook may be holding a login.
One line. It takes the latest release, unpacks it into ~/.gift, and runs the setup and install steps from there — so it asks for the public delivery URL as it goes, and leaves the gift command on your PATH. Node 18 or newer, curl and unzip; gift serve keeps the server under PM2, so npm i -g pm2 if it is not there yet.
curl -fsSL https://raw.githubusercontent.com/lhypds/gift/master/get.sh | bash
GIFT_INSTALL_DIR unpacks it somewhere other than ~/.gift, and GIFT_INSTALL_VERSION=v0.0.1 pins a release rather than taking the newest.
Upgrading is the same line again: config.json, hooks.json and the logs are carried across, so the webhook secret and the hooks you configured survive the swap. A download that fails leaves the install that was working exactly where it was.
~/.gift/uninstall.sh takes the gift command back off again, and deleting ~/.gift removes the rest; from a checkout it is ./uninstall.sh.