unirepo-cli 0.1.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 Tadej Satler
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,216 @@
1
+ # unirepo
2
+
3
+ [![npm version](https://img.shields.io/npm/v/unirepo-cli)](https://www.npmjs.com/package/unirepo-cli)
4
+ [![CI](https://github.com/Poko18/unirepo/actions/workflows/ci.yml/badge.svg)](https://github.com/Poko18/unirepo/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ **The workspace for cross-repo coding.**
8
+
9
+ unirepo turns a set of GitHub repos into a single unified workspace. Edit code across any of them, commit once, and push updates back to their original repos — all in one branch.
10
+
11
+ Built for the era of AI coding agents: give your agent one place to work, and let it refactor, ship features, and update shared APIs across your entire stack.
12
+
13
+
14
+ ## Why unirepo?
15
+
16
+ **Before unirepo:**
17
+
18
+ ```
19
+ ~/work/api ← clone, checkout branch, edit
20
+ ~/work/web ← clone, checkout same branch, edit
21
+ ~/work/shared ← clone, checkout same branch, edit
22
+ # remember to push all three, open three PRs
23
+ ```
24
+
25
+ **With unirepo:**
26
+
27
+ ```
28
+ ~/work/my-workspace/
29
+ ├── api/ ← edit
30
+ ├── web/ ← edit
31
+ └── shared/ ← edit
32
+ # one commit, one push — all three repos updated
33
+ ```
34
+
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ npx unirepo-cli init my-workspace \
40
+ https://github.com/org/api.git \
41
+ https://github.com/org/web.git
42
+ ```
43
+
44
+ This creates:
45
+
46
+ ```
47
+ my-workspace/
48
+ ├── api/ ← from github.com/org/api
49
+ ├── web/ ← from github.com/org/web
50
+ ├── AGENTS.md ← workflow guide for humans and agents
51
+ └── .gitignore
52
+ ```
53
+
54
+ Then work across repos as if they were one:
55
+
56
+ ```bash
57
+ cd my-workspace
58
+ # edit files in api/ and web/
59
+ git add . && git commit -m "feat: update shared types"
60
+ unirepo push
61
+ ```
62
+
63
+ That's it. Each subtree gets pushed to its upstream repo automatically.
64
+
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ npm install -g unirepo-cli
70
+ ```
71
+
72
+ Once installed, the command is just `unirepo`:
73
+
74
+ ```bash
75
+ unirepo --version
76
+ ```
77
+
78
+ Or use without installing:
79
+
80
+ ```bash
81
+ npx unirepo-cli <command>
82
+ ```
83
+
84
+
85
+ ## Commands
86
+
87
+ | Command | Description |
88
+ | --- | --- |
89
+ | `init <dir> <repo...>` | Create a new workspace from one or more repos |
90
+ | `add <repo>` | Add another repo to the workspace |
91
+ | `pull [subtree...]` | Pull upstream changes into tracked subtrees |
92
+ | `status` | Show subtrees, branches, and what changed |
93
+ | `branch [name]` | Create or show the current push branch |
94
+ | `push [subtree...]` | Push changed subtrees upstream |
95
+ | `version` | Show CLI version |
96
+
97
+
98
+ ## Workflow
99
+
100
+ A typical session looks like this:
101
+
102
+ ```bash
103
+ # 1. Create a branch for your work
104
+ unirepo branch feature-auth
105
+
106
+ # 2. Pull latest upstream changes
107
+ unirepo pull
108
+
109
+ # 3. Edit files across repos
110
+ vim api/src/auth.js web/src/login.tsx
111
+
112
+ # 4. Commit in the monorepo
113
+ git add . && git commit -m "feat: add OAuth flow"
114
+
115
+ # 5. Check what will be pushed
116
+ unirepo status
117
+ unirepo push --dry-run
118
+
119
+ # 6. Push to upstream repos
120
+ unirepo push
121
+
122
+ # 7. Open one PR per upstream repo
123
+ ```
124
+
125
+ The branch name you create in the workspace is reused when pushing to each upstream repo.
126
+
127
+
128
+ ## Command Reference
129
+
130
+ ### init
131
+
132
+ ```bash
133
+ unirepo init <dir> <repo-url> [repo-url...]
134
+ ```
135
+
136
+ Creates a new git repo at `<dir>`, imports each URL as a subtree, and generates an `AGENTS.md` workflow guide.
137
+
138
+ | Flag | Effect |
139
+ | --- | --- |
140
+ | `--full-history` | Import full git history (default: squash) |
141
+
142
+ ### add
143
+
144
+ ```bash
145
+ unirepo add <repo-url> [--prefix <name>] [--branch <name>]
146
+ ```
147
+
148
+ Adds a repo to an existing workspace. The directory name defaults to the repo name.
149
+
150
+ | Flag | Effect |
151
+ | --- | --- |
152
+ | `--prefix <name>` | Override the subtree directory name |
153
+ | `--branch <name>` | Import from a specific upstream branch |
154
+ | `--full-history` | Import full git history |
155
+
156
+ ### pull
157
+
158
+ ```bash
159
+ unirepo pull [subtree...]
160
+ ```
161
+
162
+ Pulls upstream changes. Without arguments, pulls all tracked subtrees.
163
+
164
+ | Flag | Effect |
165
+ | --- | --- |
166
+ | `--branch <name>` | Pull a specific upstream branch |
167
+ | `--full-history` | Pull full history instead of squash |
168
+
169
+ ### status
170
+
171
+ ```bash
172
+ unirepo status [--json]
173
+ ```
174
+
175
+ Shows tracked subtrees, their upstream branches, the current push branch, and which subtrees have changes.
176
+
177
+ ### branch
178
+
179
+ ```bash
180
+ unirepo branch [name]
181
+ ```
182
+
183
+ With a name: creates and switches to a new branch. Without: shows the current branch and push targets.
184
+
185
+ ### push
186
+
187
+ ```bash
188
+ unirepo push [subtree...] [--dry-run]
189
+ ```
190
+
191
+ Pushes changed subtrees upstream. Without arguments, auto-detects which subtrees have changes.
192
+
193
+ | Flag | Effect |
194
+ | --- | --- |
195
+ | `--branch <name>` | Override the upstream branch name |
196
+ | `--dry-run` | Show what would run without executing |
197
+
198
+
199
+ ## How It Works
200
+
201
+ unirepo is a thin wrapper around [`git subtree`](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging#_subtree_merge). Each repo you add becomes a directory in your workspace with a matching git remote. When you push, unirepo splits your commits per subtree and pushes each to its upstream.
202
+
203
+ There's no lock-in — the workspace is a standard git repo. You can always fall back to raw `git subtree` commands.
204
+
205
+
206
+ ## Development
207
+
208
+ ```bash
209
+ npm test # run tests
210
+ npm run build # syntax check
211
+ ```
212
+
213
+
214
+ ## License
215
+
216
+ MIT — see [LICENSE](LICENSE).
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "unirepo-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for creating and managing git-subtree monorepos — run your agents across repos",
5
+ "type": "module",
6
+ "bin": {
7
+ "unirepo": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src/"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "start": "node src/index.js",
17
+ "test": "node --test",
18
+ "build": "node --check src/*.js src/commands/*.js"
19
+ },
20
+ "keywords": [
21
+ "git",
22
+ "subtree",
23
+ "monorepo",
24
+ "cli",
25
+ "developer-tools",
26
+ "agents"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/Poko18/unirepo.git"
31
+ },
32
+ "homepage": "https://github.com/Poko18/unirepo#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/Poko18/unirepo/issues"
35
+ },
36
+ "author": "Poko18",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "chalk": "^5.3.0"
40
+ }
41
+ }
@@ -0,0 +1,45 @@
1
+ import { git, detectDefaultBranch, extractRepoName } from '../git.js';
2
+ import { validateGitSubtree, validateUrls, validateInsideMonorepo, validateNameAvailable, validateReachable } from '../validate.js';
3
+ import * as ui from '../ui.js';
4
+
5
+ export async function runAdd({ url, prefix, branch, fullHistory }) {
6
+ const cwd = process.cwd();
7
+
8
+ // ── Preflight ────────────────────────────────────────────────────────────
9
+ ui.header('Adding repository');
10
+ ui.blank();
11
+
12
+ ui.step(1, 4, 'Running preflight checks...');
13
+ validateGitSubtree();
14
+ validateInsideMonorepo(cwd);
15
+ validateUrls([url]);
16
+
17
+ const name = prefix || extractRepoName(url);
18
+ validateNameAvailable(name, cwd);
19
+ ui.info(`Checking ${url}...`);
20
+ validateReachable(url);
21
+ ui.success('All checks passed');
22
+
23
+ // ── Detect branch ────────────────────────────────────────────────────────
24
+ ui.step(2, 4, branch ? `Using upstream branch for ${name}...` : `Detecting default branch for ${name}...`);
25
+ const upstreamBranch = branch || detectDefaultBranch(url);
26
+ ui.repoDetail('Branch', upstreamBranch);
27
+
28
+ // ── Add remote + fetch ───────────────────────────────────────────────────
29
+ ui.step(3, 4, 'Adding remote and fetching...');
30
+ git(`remote add "${name}" "${url}"`, { cwd, silent: true });
31
+
32
+ if (fullHistory) {
33
+ ui.repoDetail('Mode', 'full history');
34
+ git(`fetch --no-tags "${name}" "${upstreamBranch}"`, { cwd });
35
+ git(`subtree add --prefix="${name}" "${name}" "${upstreamBranch}"`, { cwd });
36
+ } else {
37
+ ui.repoDetail('Mode', 'shallow + squash');
38
+ git(`fetch --no-tags --depth 1 "${name}" "${upstreamBranch}"`, { cwd });
39
+ git(`subtree add --squash --prefix="${name}" "${name}" "${upstreamBranch}"`, { cwd });
40
+ }
41
+
42
+ // ── Done ─────────────────────────────────────────────────────────────────
43
+ ui.step(4, 4, 'Done!');
44
+ ui.addSummary(name, url);
45
+ }
@@ -0,0 +1,54 @@
1
+ import { git, getCurrentBranch, getSubtreePrefixes, getRemoteBranch } from '../git.js';
2
+ import { validateInsideMonorepo } from '../validate.js';
3
+ import * as ui from '../ui.js';
4
+
5
+ export async function runBranch({ name }) {
6
+ const cwd = process.cwd();
7
+
8
+ validateInsideMonorepo(cwd);
9
+
10
+ const subtrees = getSubtreePrefixes(cwd);
11
+ const currentBranch = getCurrentBranch(cwd);
12
+
13
+ if (!name) {
14
+ // No name given — show current state
15
+ ui.header('Branch');
16
+ ui.blank();
17
+ ui.info(`Current branch: ${currentBranch}`);
18
+ ui.blank();
19
+ for (const s of subtrees) {
20
+ const upstream = getRemoteBranch(cwd, s.name) || 'unknown';
21
+ ui.info(` ${s.name} upstream: ${upstream} push target: ${currentBranch}`);
22
+ }
23
+ ui.blank();
24
+ ui.info('Use "unirepo branch <name>" to switch all subtrees to a new branch.');
25
+ ui.blank();
26
+ return;
27
+ }
28
+
29
+ ui.header('Branch');
30
+ ui.blank();
31
+
32
+ if (currentBranch === name) {
33
+ ui.info(`Already on branch "${name}"`);
34
+ ui.blank();
35
+ return;
36
+ }
37
+
38
+ // Create and switch to the new branch locally
39
+ git(`checkout -b "${name}"`, { cwd, silent: true });
40
+ ui.success(`Switched to new branch "${name}"`);
41
+ ui.blank();
42
+
43
+ for (const s of subtrees) {
44
+ ui.success(`${s.name} → push will target "${name}" upstream`);
45
+ }
46
+
47
+ ui.blank();
48
+ ui.info('Next steps:');
49
+ ui.info(' 1. Make changes in subtree directories');
50
+ ui.info(' 2. Commit in the monorepo');
51
+ ui.info(` 3. unirepo push`);
52
+ ui.info(' 4. Open one PR per upstream repo');
53
+ ui.blank();
54
+ }
@@ -0,0 +1,95 @@
1
+ import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
2
+ import { resolve, join } from 'node:path';
3
+ import { git, detectDefaultBranch, extractRepoName } from '../git.js';
4
+ import { validateGitSubtree, validateUrls, validateNoDuplicateNames, validateReachable } from '../validate.js';
5
+ import { AGENTS_MD, GITIGNORE } from '../templates.js';
6
+ import * as ui from '../ui.js';
7
+
8
+ export async function runInit({ dir, repos, fullHistory }) {
9
+ const absDir = resolve(dir);
10
+
11
+ if (existsSync(absDir)) {
12
+ throw new Error(`Directory "${dir}" already exists. Choose a different name or remove it first.`);
13
+ }
14
+
15
+ // ── Preflight ────────────────────────────────────────────────────────────
16
+ ui.header('Creating monorepo');
17
+ ui.blank();
18
+
19
+ ui.step(1, 3 + repos.length, 'Running preflight checks...');
20
+ validateGitSubtree();
21
+ validateUrls(repos);
22
+ validateNoDuplicateNames(repos);
23
+ for (const url of repos) {
24
+ ui.info(`Checking ${url}...`);
25
+ validateReachable(url);
26
+ }
27
+ ui.success('All checks passed');
28
+
29
+ // From here on, clean up the directory if anything fails
30
+ let created = false;
31
+ try {
32
+ // ── Initialize repo ──────────────────────────────────────────────────────
33
+ ui.step(2, 3 + repos.length, 'Initializing git repository...');
34
+ mkdirSync(absDir, { recursive: true });
35
+ created = true;
36
+ git('init', { cwd: absDir, silent: true });
37
+ git('commit --allow-empty -m "chore: initial commit"', { cwd: absDir, silent: true });
38
+ ui.repoDetail('Location', absDir);
39
+
40
+ // ── Scaffold files ───────────────────────────────────────────────────────
41
+ ui.step(3, 3 + repos.length, 'Writing scaffold files...');
42
+ const scaffoldFiles = [];
43
+
44
+ writeFileSync(join(absDir, 'AGENTS.md'), AGENTS_MD);
45
+ scaffoldFiles.push('AGENTS.md');
46
+
47
+ writeFileSync(join(absDir, '.gitignore'), GITIGNORE);
48
+ scaffoldFiles.push('.gitignore');
49
+
50
+ git(`add ${scaffoldFiles.join(' ')}`, { cwd: absDir, silent: true });
51
+ git('commit -m "chore: add monorepo scaffolding"', { cwd: absDir, silent: true });
52
+ ui.success(`Wrote ${scaffoldFiles.join(', ')}`);
53
+
54
+ // ── Import repos ─────────────────────────────────────────────────────────
55
+ for (let i = 0; i < repos.length; i++) {
56
+ const url = repos[i];
57
+ const name = extractRepoName(url);
58
+ const stepNum = 4 + i;
59
+
60
+ ui.blank();
61
+ ui.repoStep(stepNum, 3 + repos.length, name, 'Importing');
62
+ ui.repoDetail('URL', url);
63
+
64
+ // Detect default branch
65
+ const branch = detectDefaultBranch(url);
66
+ ui.repoDetail('Branch', branch);
67
+
68
+ // Add remote
69
+ git(`remote add "${name}" "${url}"`, { cwd: absDir, silent: true });
70
+
71
+ // Fetch
72
+ if (fullHistory) {
73
+ ui.repoDetail('Mode', 'full history');
74
+ git(`fetch --no-tags "${name}" "${branch}"`, { cwd: absDir });
75
+ git(`subtree add --prefix="${name}" "${name}" "${branch}"`, { cwd: absDir });
76
+ } else {
77
+ ui.repoDetail('Mode', 'shallow + squash');
78
+ git(`fetch --no-tags --depth 1 "${name}" "${branch}"`, { cwd: absDir });
79
+ git(`subtree add --squash --prefix="${name}" "${name}" "${branch}"`, { cwd: absDir });
80
+ }
81
+
82
+ ui.success(`${name} imported`);
83
+ }
84
+ } catch (err) {
85
+ // Clean up partially created directory so the user can retry
86
+ if (created) {
87
+ try { rmSync(absDir, { recursive: true, force: true }); } catch {}
88
+ }
89
+ throw err;
90
+ }
91
+
92
+ // ── Summary ──────────────────────────────────────────────────────────────
93
+ const subtreeNames = repos.map((url) => extractRepoName(url));
94
+ ui.initSummary(absDir, repos.length, subtreeNames);
95
+ }
@@ -0,0 +1,65 @@
1
+ import { git, getRemoteBranch, getSubtreePrefixes } from '../git.js';
2
+ import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
3
+ import * as ui from '../ui.js';
4
+
5
+ export async function runPull({ subtrees: requestedSubtrees, branch, fullHistory }) {
6
+ const cwd = process.cwd();
7
+
8
+ ui.header('Pulling subtrees');
9
+ ui.blank();
10
+
11
+ validateGitSubtree();
12
+ validateInsideMonorepo(cwd);
13
+
14
+ const allPrefixes = getSubtreePrefixes(cwd);
15
+ let toPull;
16
+
17
+ if (requestedSubtrees && requestedSubtrees.length > 0) {
18
+ const prefixNames = new Set(allPrefixes.map((prefix) => prefix.name));
19
+ for (const name of requestedSubtrees) {
20
+ if (!prefixNames.has(name)) {
21
+ throw new Error(`"${name}" is not a tracked subtree. Run "unirepo status" to see available subtrees.`);
22
+ }
23
+ }
24
+ toPull = allPrefixes.filter((prefix) => requestedSubtrees.includes(prefix.name));
25
+ } else {
26
+ toPull = allPrefixes;
27
+ }
28
+
29
+ if (toPull.length === 0) {
30
+ ui.info('No tracked subtrees found. Nothing to pull.');
31
+ ui.blank();
32
+ return;
33
+ }
34
+
35
+ let succeeded = 0;
36
+ let failed = 0;
37
+
38
+ for (let i = 0; i < toPull.length; i++) {
39
+ const prefix = toPull[i];
40
+ const upstreamBranch = branch || getRemoteBranch(cwd, prefix.name) || 'main';
41
+ const squashFlag = fullHistory ? '' : '--squash ';
42
+
43
+ ui.repoStep(i + 1, toPull.length, prefix.name, 'Pulling');
44
+ ui.repoDetail('Branch', upstreamBranch);
45
+ ui.repoDetail('Mode', fullHistory ? 'full history' : 'squash');
46
+
47
+ try {
48
+ git(`subtree pull ${squashFlag}--prefix="${prefix.name}" "${prefix.name}" "${upstreamBranch}"`, { cwd });
49
+ ui.success(`${prefix.name} pulled`);
50
+ succeeded++;
51
+ } catch (err) {
52
+ ui.error(`Failed to pull ${prefix.name}: ${err.message}`);
53
+ failed++;
54
+ }
55
+
56
+ ui.blank();
57
+ }
58
+
59
+ if (failed === 0) {
60
+ ui.success(`All ${succeeded} subtree(s) pulled`);
61
+ } else {
62
+ ui.warning(`${succeeded} pulled, ${failed} failed`);
63
+ }
64
+ ui.blank();
65
+ }
@@ -0,0 +1,90 @@
1
+ import { git, getCurrentBranch, getSubtreePrefixes, getChangedSubtrees, hasUncommittedChanges } from '../git.js';
2
+ import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
3
+ import * as ui from '../ui.js';
4
+
5
+ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
6
+ const cwd = process.cwd();
7
+
8
+ // ── Preflight ────────────────────────────────────────────────────────────
9
+ ui.header('Pushing subtrees');
10
+ ui.blank();
11
+
12
+ validateGitSubtree();
13
+ validateInsideMonorepo(cwd);
14
+
15
+ const currentBranch = getCurrentBranch(cwd);
16
+ const pushBranch = branch || currentBranch;
17
+
18
+ // ── Check for uncommitted work ────────────────────────────────────────────
19
+ const hasUncommitted = hasUncommittedChanges(cwd);
20
+ if (hasUncommitted) {
21
+ ui.warning('You have uncommitted changes. Only committed work will be pushed upstream.');
22
+ ui.info('Commit your changes first, then run push again.');
23
+ ui.blank();
24
+ }
25
+
26
+ // ── Determine which subtrees to push ─────────────────────────────────────
27
+ let toPush;
28
+
29
+ if (requestedSubtrees && requestedSubtrees.length > 0) {
30
+ // Validate that requested subtrees exist
31
+ const allPrefixes = getSubtreePrefixes(cwd);
32
+ const prefixNames = new Set(allPrefixes.map((p) => p.name));
33
+ for (const name of requestedSubtrees) {
34
+ if (!prefixNames.has(name)) {
35
+ throw new Error(`"${name}" is not a tracked subtree. Run "unirepo status" to see available subtrees.`);
36
+ }
37
+ }
38
+ toPush = allPrefixes.filter((p) => requestedSubtrees.includes(p.name));
39
+ } else {
40
+ // Auto-detect changed subtrees
41
+ toPush = getChangedSubtrees(cwd);
42
+ if (toPush.length === 0) {
43
+ ui.info('No changed subtrees detected. Nothing to push.');
44
+ if (!hasUncommitted) {
45
+ ui.info('Make changes inside subtree directories and commit before pushing.');
46
+ }
47
+ ui.blank();
48
+ return;
49
+ }
50
+ }
51
+
52
+ ui.info(`Branch: ${pushBranch}`);
53
+ ui.info(`Subtrees to push: ${toPush.map((p) => p.name).join(', ')}`);
54
+ ui.blank();
55
+
56
+ // ── Dry run ──────────────────────────────────────────────────────────────
57
+ if (dryRun) {
58
+ const commands = toPush.map(
59
+ (p) => `git subtree push --prefix="${p.name}" "${p.name}" "${pushBranch}"`
60
+ );
61
+ ui.dryRun(commands);
62
+ return;
63
+ }
64
+
65
+ // ── Push each subtree serially ───────────────────────────────────────────
66
+ let succeeded = 0;
67
+ let failed = 0;
68
+
69
+ for (const prefix of toPush) {
70
+ ui.pushStart(prefix.name, pushBranch);
71
+ ui.pushSlow();
72
+ try {
73
+ git(`subtree push --prefix="${prefix.name}" "${prefix.name}" "${pushBranch}"`, { cwd });
74
+ ui.success(`${prefix.name} pushed`);
75
+ succeeded++;
76
+ } catch (err) {
77
+ ui.error(`Failed to push ${prefix.name}: ${err.message}`);
78
+ failed++;
79
+ }
80
+ }
81
+
82
+ // ── Summary ──────────────────────────────────────────────────────────────
83
+ ui.blank();
84
+ if (failed === 0) {
85
+ ui.success(`All ${succeeded} subtree(s) pushed to branch "${pushBranch}"`);
86
+ } else {
87
+ ui.warning(`${succeeded} pushed, ${failed} failed`);
88
+ }
89
+ ui.blank();
90
+ }
@@ -0,0 +1,29 @@
1
+ import { getCurrentBranch, getSubtreePrefixes, getChangedSubtrees, getRemoteBranch } from '../git.js';
2
+ import { validateInsideMonorepo } from '../validate.js';
3
+ import * as ui from '../ui.js';
4
+
5
+ export async function runStatus({ json }) {
6
+ const cwd = process.cwd();
7
+
8
+ validateInsideMonorepo(cwd);
9
+
10
+ const branch = getCurrentBranch(cwd);
11
+ const prefixes = getSubtreePrefixes(cwd);
12
+ const changed = getChangedSubtrees(cwd);
13
+ const changedNames = new Set(changed.map((c) => c.name));
14
+
15
+ const subtrees = prefixes.map((p) => ({
16
+ name: p.name,
17
+ url: p.url,
18
+ upstream: getRemoteBranch(cwd, p.name) || 'unknown',
19
+ pushBranch: branch,
20
+ changed: changedNames.has(p.name),
21
+ }));
22
+
23
+ if (json) {
24
+ console.log(JSON.stringify({ pushBranch: branch, subtrees }, null, 2));
25
+ return;
26
+ }
27
+
28
+ ui.subtreeTable(subtrees, branch);
29
+ }