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 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.
@@ -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)