uberepo 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/uberepo.mjs +21 -0
- package/dist/cli.mjs +7749 -0
- package/package.json +40 -0
- package/template/.agents/skills/using-uberepo/SKILL.md +142 -0
- package/template/.agents/skills/using-uberepo/reference.md +487 -0
- package/template/.claude/skills/using-uberepo/SKILL.md +142 -0
- package/template/.claude/skills/using-uberepo/reference.md +487 -0
- package/template/AGENTS.md +36 -0
- package/template/CLAUDE.md +1 -0
- package/template/gitignore +5 -0
- package/template/ubertask.yml +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mateusz Pietrzak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<p align="center"><img src=".github/assets/banner.svg" alt="überepo — switch tasks, not branches" width="100%"></p>
|
|
2
|
+
<p align="center">A multi-repo workspace where one task owns one branch in <i>every</i> repo —<br>and every command speaks JSON, so your coding agent can drive it too.</p>
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## The problem with five repos
|
|
7
|
+
|
|
8
|
+
Marketing rebranded. "Acme" is now "Akko" — new logo, new name, same Tuesday. The old name lives in five repos. So you start the dance:
|
|
9
|
+
```text
|
|
10
|
+
cd api && git checkout -b big-rename
|
|
11
|
+
cd ../web && git checkout -b big-rename
|
|
12
|
+
cd ../types && git checkout -b big-rename
|
|
13
|
+
cd ../svc-a && git checkout -b big-rename
|
|
14
|
+
cd ../svc-b && git checkout -b big-rename
|
|
15
|
+
```
|
|
16
|
+
Then a "quick fix" lands on `main` and you do the whole dance again in reverse, stashing as you go. One stray `git checkout` and you're committing to `main` like an animal.
|
|
17
|
+
|
|
18
|
+
Here's the mismatch: **a branch is a per-repo idea. Your task isn't.**
|
|
19
|
+
Your task is "ship the rebrand" — it doesn't care that it happens to touch five repositories.
|
|
20
|
+
|
|
21
|
+
## So überepo flips it
|
|
22
|
+
|
|
23
|
+
One task = one branch in **every** repo, each in its own git worktree:
|
|
24
|
+
```bash
|
|
25
|
+
uberepo open big-rename
|
|
26
|
+
```
|
|
27
|
+
That's the five checkouts. One command.
|
|
28
|
+
Every repo gets a `task/big-rename` worktree under `tasks/big-rename/`; you switch tasks by changing folders, and your `main` checkout never moves. On disk:
|
|
29
|
+
```text
|
|
30
|
+
my-workspace/
|
|
31
|
+
├── uberepo.json # the manifest: which repos, which hooks, what to carry
|
|
32
|
+
├── source/ # one canonical clone per repo
|
|
33
|
+
│ ├── api/
|
|
34
|
+
│ └── web/
|
|
35
|
+
└── tasks/
|
|
36
|
+
└── big-rename/ # one task...
|
|
37
|
+
├── ubertask.yml # ...its handoff note
|
|
38
|
+
├── api/ # ...and a worktree per repo, on task/big-rename
|
|
39
|
+
└── web/
|
|
40
|
+
```
|
|
41
|
+
**Tasks are first-class. Repos are just the participants.**
|
|
42
|
+
|
|
43
|
+
### More than one branch in a repo
|
|
44
|
+
|
|
45
|
+
Usually a repo joins a task once, on one branch. When you need two branches in
|
|
46
|
+
the *same* repo for one task — two stacked PRs, a fix and a follow-up — give each
|
|
47
|
+
an alias with `@`:
|
|
48
|
+
```bash
|
|
49
|
+
uberepo open big-rename --repos web@strings web@logos api
|
|
50
|
+
```
|
|
51
|
+
`web` now contributes two participants, each its own worktree and branch, while
|
|
52
|
+
`api` stays a plain single-branch participant:
|
|
53
|
+
```text
|
|
54
|
+
tasks/big-rename/
|
|
55
|
+
├── ubertask.yml
|
|
56
|
+
├── web@strings/ # branch task/big-rename@strings
|
|
57
|
+
├── web@logos/ # branch task/big-rename@logos
|
|
58
|
+
└── api/ # branch task/big-rename
|
|
59
|
+
```
|
|
60
|
+
The `repo@alias` token is the same everywhere — the `--repos` argument, the
|
|
61
|
+
branch leaf (`task/big-rename@strings`), and the worktree folder (flat, one level
|
|
62
|
+
deep). Both branches push from the one shared `source/web` clone; `ship` opens a
|
|
63
|
+
PR per branch, `close` tears down both worktrees and keeps the clone. A bare
|
|
64
|
+
`web` (no `@`) is unchanged.
|
|
65
|
+
|
|
66
|
+
### Stacked PRs
|
|
67
|
+
|
|
68
|
+
Those two branches are often a *stack*: `logos` builds on `strings`, so its PR
|
|
69
|
+
should target `strings`'s branch, not `main`. Declare the edge once, on `open`:
|
|
70
|
+
```bash
|
|
71
|
+
uberepo open big-rename --stack web@logos=web@strings
|
|
72
|
+
```
|
|
73
|
+
That records `web@logos`'s base as the sibling `web@strings` in the note (a
|
|
74
|
+
stack edge, not a remote ref). From then on the whole lifecycle keeps the stack
|
|
75
|
+
honest:
|
|
76
|
+
- **`ship`** opens `web@logos`'s PR against `task/big-rename@strings` (the
|
|
77
|
+
parent's branch), and pushes the parent first — a child whose parent isn't on
|
|
78
|
+
the remote yet is skipped with *"parent not on remote — ship it first"*.
|
|
79
|
+
- **`sync`** rebases the forest bottom-up — parent first, then each child onto
|
|
80
|
+
its freshly-moved parent — so a rebase upstream ripples through the stack
|
|
81
|
+
without flattening it.
|
|
82
|
+
- **`status`/`diff`/`context`** nest the child under its parent in a `└─` tree,
|
|
83
|
+
and `diff`/`context` measure the child's commits against the parent's branch,
|
|
84
|
+
not `main`.
|
|
85
|
+
|
|
86
|
+
The edge must stay same-repo, in-scope, and acyclic — a cross-repo, out-of-scope,
|
|
87
|
+
or cycle-forming `--stack` is rejected when you declare it. A participant with no
|
|
88
|
+
`--stack` is an ordinary root, exactly as before.
|
|
89
|
+
|
|
90
|
+
## Why not a monorepo?
|
|
91
|
+
|
|
92
|
+
Sometimes you can't merge the repos — separate owners, separate CI, separate deploy cadences — so überepo works with the ones you're stuck with, each keeping its own conventions and PR flow.
|
|
93
|
+
|
|
94
|
+
**If you *can* merge everything into a monorepo, do that. überepo is for when you can't.**
|
|
95
|
+
## Why not just "use worktrees"?
|
|
96
|
+
|
|
97
|
+
If it's one repo and one session, do exactly that.
|
|
98
|
+
Across five repos it falls apart: every session invents its own branch names and layout, and none of it survives to the next session or the next agent.
|
|
99
|
+
|
|
100
|
+
überepo is "use worktrees" written down once. A task is one folder with a worktree per repo inside, the handoff note and `status --json` tell a fresh session where things stand, and the chores are commands: `sync` rebases everything, `ship` pushes and opens the PRs, `close` tears it down.
|
|
101
|
+
|
|
102
|
+
**A `CLAUDE.md` can hold a convention. It can't hold machinery.**
|
|
103
|
+
## Quickstart
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install -g uberepo
|
|
107
|
+
```
|
|
108
|
+
```bash
|
|
109
|
+
# ── once ──────────────────────────────────────────
|
|
110
|
+
# 1. new workspace
|
|
111
|
+
uberepo init my-workspace && cd my-workspace
|
|
112
|
+
# 2. register repos (yes, the org is still acme)
|
|
113
|
+
uberepo add https://github.com/acme/api.git https://github.com/acme/web.git
|
|
114
|
+
# 3. clone into source/
|
|
115
|
+
uberepo clone
|
|
116
|
+
|
|
117
|
+
# ── every task ────────────────────────────────────
|
|
118
|
+
# 4. branch + worktree in every repo
|
|
119
|
+
uberepo open big-rename --goal "Acme → Akko. Every string, every logo, every invoice."
|
|
120
|
+
# 5. do the work; commits are yours, in each worktree
|
|
121
|
+
# 6. rebase onto fresh upstreams
|
|
122
|
+
uberepo sync big-rename
|
|
123
|
+
# 7. push + draft PR per repo (needs gh)
|
|
124
|
+
uberepo ship big-rename --title "Acme → Akko"
|
|
125
|
+
# 8. PRs merged? tear it down
|
|
126
|
+
uberepo close big-rename
|
|
127
|
+
```
|
|
128
|
+
**The loop, end to end:**
|
|
129
|
+
```text
|
|
130
|
+
╭──────╮ ╭──────╮ ╭──────╮ ╭───────╮
|
|
131
|
+
│ open │ ───▶ │ sync │ ───▶ │ ship │ ───▶ │ close │
|
|
132
|
+
╰──────╯ ╰───▲──╯ ╰──┬───╯ ╰───────╯
|
|
133
|
+
│ │
|
|
134
|
+
╰────────────╯
|
|
135
|
+
iterate until merged
|
|
136
|
+
```
|
|
137
|
+
## Built so your agent can drive it
|
|
138
|
+
|
|
139
|
+
überepo doesn't just tolerate coding agents — it's built for them.
|
|
140
|
+
- **Tasks carry handoff notes.** Every task gets `tasks/<task>/ubertask.yml` — goal, scope, tickets, decisions, blockers; one session writes it, the next (or you, on Monday morning) reads it and knows where things stand.
|
|
141
|
+
- **One command rehydrates a session.** `uberepo context <task>` replays the whole handoff — the note, each repo's commits-ahead and diffstat, each branch's PR state — as a paste-ready markdown brief, or `--json` for the agent.
|
|
142
|
+
- **Runs are idempotent and resumable.** `open`, `clone`, and `ship` skip what's already done — an agent can re-run after a crash and not make a mess.
|
|
143
|
+
- **It ships its own playbook.** `uberepo init` stamps a `using-uberepo` skill into the workspace, so agents know the lifecycle without you explaining it. (`--no-agents` skips it.)
|
|
144
|
+
- **Every command speaks JSON.** Add `--json` to anything and get structured output instead of pretty text — `uberepo status --json`:
|
|
145
|
+
```json
|
|
146
|
+
[{
|
|
147
|
+
"name": "big-rename",
|
|
148
|
+
"repos": [{ "name": "api", "branch": "task/big-rename", "dirty": false }],
|
|
149
|
+
"note": { "goal": "Acme → Akko. Every string, every logo, every invoice." }
|
|
150
|
+
}]
|
|
151
|
+
```
|
|
152
|
+
And the handoff note itself, `tasks/big-rename/ubertask.yml` — the "why"; git holds the "what":
|
|
153
|
+
```yaml
|
|
154
|
+
goal: |
|
|
155
|
+
Acme → Akko. Every string, every logo, every invoice.
|
|
156
|
+
repos:
|
|
157
|
+
- api
|
|
158
|
+
- web
|
|
159
|
+
branches:
|
|
160
|
+
api:
|
|
161
|
+
name: feat/akko-rename
|
|
162
|
+
adopted: true
|
|
163
|
+
base: develop
|
|
164
|
+
tickets:
|
|
165
|
+
- https://example.com/ACME-1234
|
|
166
|
+
decisions:
|
|
167
|
+
- note: |
|
|
168
|
+
The database stays acme_prod. We are not renaming the database. Ever.
|
|
169
|
+
repo: api
|
|
170
|
+
```
|
|
171
|
+
## Commands
|
|
172
|
+
|
|
173
|
+
**Set up the workspace**
|
|
174
|
+
|
|
175
|
+
| Command | What it does |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `uberepo init [<name>] [--no-agents]` | Create a workspace. `--no-agents` skips the agent skill files. |
|
|
178
|
+
| `uberepo add <repo>...` | Register one or more repository URLs. |
|
|
179
|
+
| `uberepo remove <repo>` | Unregister a repository. |
|
|
180
|
+
| `uberepo sources` | List registered repos and their clone status. |
|
|
181
|
+
| `uberepo clone` | Clone every registered repo into `source/` — or just `--repos <name>...`. Idempotent. |
|
|
182
|
+
| `uberepo pull` | Fast-forward all source clones (skips dirty ones). |
|
|
183
|
+
|
|
184
|
+
**Run a task**
|
|
185
|
+
|
|
186
|
+
| Command | What it does |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `uberepo open <task>` | Branch + worktree in every repo. Takes `--goal`, `--repos`, `--from`, `--branch`, `--stack`; repos scoped via `--repos` clone on demand. A `--repos repo@alias` token gives one repo a second branch in the task ([below](#more-than-one-branch-in-a-repo)). `--branch <repo>=<name>` (or a bare `--branch <name>` for all repos) **adopts** an existing branch instead of creating `task/<task>` — `close`/`prune` then keep that branch. `--stack <child>=<parent>` stacks one participant's branch on a sibling's, so `ship`/`sync` target and rebase it against the parent ([below](#stacked-prs)). |
|
|
189
|
+
| `uberepo status [<task>]` | Show open tasks, their branches, and clean/dirty state. |
|
|
190
|
+
| `uberepo diff <task>` | Show the task's footprint: commits ahead + diffstat per repo. |
|
|
191
|
+
| `uberepo exec <task> -- <cmd>...` | Run one command inside every one of the task's worktrees. `--repos` narrows it; `--bail` stops at the first failure. Exits non-zero if any repo's command did. |
|
|
192
|
+
| `uberepo context <task>` | Everything to resume a task — note, per-repo state, PR state — as a paste-ready markdown brief. |
|
|
193
|
+
| `uberepo sync <task>` | Rebase the task's worktrees onto fresh upstreams. `--check` forecasts the conflicts without rebasing. |
|
|
194
|
+
| `uberepo ship <task>` | Push every branch and open a draft PR per repo (needs `gh`). |
|
|
195
|
+
| `uberepo close <task>` | Remove the worktrees and delete the task branch. |
|
|
196
|
+
| `uberepo prune` | Remove merged-and-clean tasks. Previews by default; `--force` to commit. |
|
|
197
|
+
|
|
198
|
+
Every command accepts `--json`. The lifecycle commands (`clone`, `open`, `sync`, `ship`, `close`) accept `--no-hooks`.
|
|
199
|
+
## How it works
|
|
200
|
+
|
|
201
|
+
No daemon. No database. No lock file. State lives in git (branches + worktrees), in `uberepo.json` (the manifest), and in `ubertask.yml` (the task note). überepo itself is a thin, opinionated layer over `git worktree`:
|
|
202
|
+
- **Worktrees do the heavy lifting.** Every task branch is a real `git worktree` checkout — überepo reads git's own registry, so there's nothing to desync.
|
|
203
|
+
- **Hooks handle the setup grind.** Wire pre-/post- hooks around every lifecycle command (`clone`, `open`, `sync`, `ship`, `close`) into `uberepo.json`; überepo fires them per repo with `UBEREPO_TASK` and `UBEREPO_REPO_*` in the environment — `npm install`, a `.env`, a test gate before `ship` ([full reference](docs/hooks.md)).
|
|
204
|
+
- **Carry brings your local config along.** Fresh worktrees hold only tracked files; list glob patterns under `carry` in `uberepo.json` — one array for every repo, or an object keyed by repo name for per-repo sets — and überepo copies the matching untracked files — `.env`, override files, local certs — from `source/<name>` into every task worktree on `open` and `sync`, never overwriting your in-task edits ([full reference](docs/carry.md)).
|
|
205
|
+
- **It never commits for you.** Branches and worktrees are überepo's job; the commits and pushes stay yours (or your agent's). A coordinator, not a backseat driver.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
Requires git ≥ 2.5 (when worktrees landed). The `gh` CLI is needed only for `ship`.
|
|
210
|
+
Licensed [MIT](LICENSE) © Mateusz Pietrzak.
|
package/bin/uberepo.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin production launcher: load the bundled CLI in-process with plain node —
|
|
3
|
+
// no tsx, no TypeScript, no child process. Node resolves the bin symlink's
|
|
4
|
+
// realpath before executing, so ../dist lands inside the installed package.
|
|
5
|
+
//
|
|
6
|
+
// Development runs from source instead: `npm run dev -- <args>` (tsx).
|
|
7
|
+
import * as fs from "node:fs"
|
|
8
|
+
import * as path from "node:path"
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
10
|
+
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const entry = path.join(here, "..", "dist", "cli.mjs")
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(entry)) {
|
|
15
|
+
console.error(
|
|
16
|
+
"uberepo: dist/cli.mjs is missing. In a checkout, run `npm run build` first (or use `npm run dev -- <args>` to run from source)."
|
|
17
|
+
)
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await import(pathToFileURL(entry).href)
|