unirepo-cli 0.5.1 → 0.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unirepo-cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "CLI tool for creating and managing git-subtree monorepos — run your agents across repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
- import { git, detectDefaultBranch, extractRepoName, setConfiguredSubtreeBranch } from '../git.js';
1
+ import { git, detectDefaultBranch, extractRepoName, getMonorepoRoot, setConfiguredSubtreeBranch } from '../git.js';
2
2
  import { validateGitSubtree, validateUrls, validateInsideMonorepo, validateNameAvailable, validateReachable } from '../validate.js';
3
3
  import * as ui from '../ui.js';
4
4
 
5
5
  export async function runAdd({ url, prefix, branch, fullHistory }) {
6
- const cwd = process.cwd();
6
+ const cwd = getMonorepoRoot(process.cwd());
7
7
 
8
8
  // ── Preflight ────────────────────────────────────────────────────────────
9
9
  ui.header('Adding repository');
@@ -2,6 +2,7 @@ import {
2
2
  git,
3
3
  getConfiguredSubtreePushBranch,
4
4
  getCurrentBranch,
5
+ getMonorepoRoot,
5
6
  getSubtreePrefixes,
6
7
  getTrackedSubtreeBranch,
7
8
  resolveSubtreePushBranch,
@@ -10,7 +11,7 @@ import { validateInsideMonorepo } from '../validate.js';
10
11
  import * as ui from '../ui.js';
11
12
 
12
13
  export async function runBranch({ name }) {
13
- const cwd = process.cwd();
14
+ const cwd = getMonorepoRoot(process.cwd());
14
15
 
15
16
  validateInsideMonorepo(cwd);
16
17
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getConfiguredSubtreePushBranch,
3
3
  getCurrentBranch,
4
+ getMonorepoRoot,
4
5
  getSubtreePrefixes,
5
6
  getChangedSubtrees,
6
7
  getTrackedSubtreeBranch,
@@ -62,7 +63,7 @@ export function planPrTargets({
62
63
  }
63
64
 
64
65
  export async function runPr({ subtrees: requestedSubtrees, title, body, base, head, draft, dryRun }) {
65
- const cwd = process.cwd();
66
+ const cwd = getMonorepoRoot(process.cwd());
66
67
 
67
68
  ui.header('Creating pull requests');
68
69
  ui.blank();
@@ -1,4 +1,4 @@
1
- import { git, getSubtreePrefixes, getTrackedSubtreeBranch, hasRemoteBranch, setConfiguredSubtreeBranch } from '../git.js';
1
+ import { git, getMonorepoRoot, getSubtreePrefixes, getTrackedSubtreeBranch, hasRemoteBranch, setConfiguredSubtreeBranch } from '../git.js';
2
2
  import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
3
3
  import * as ui from '../ui.js';
4
4
 
@@ -49,7 +49,7 @@ export function resolvePullUpstreamBranch({ requestedBranch, trackedBranch }) {
49
49
  }
50
50
 
51
51
  export async function runPull({ subtrees: requestedSubtrees, branch, fullHistory }) {
52
- const cwd = process.cwd();
52
+ const cwd = getMonorepoRoot(process.cwd());
53
53
 
54
54
  ui.header('Pulling subtrees');
55
55
  ui.blank();
@@ -2,16 +2,18 @@ import {
2
2
  git,
3
3
  getConfiguredSubtreePushBranch,
4
4
  getCurrentBranch,
5
+ getMonorepoRoot,
5
6
  getSubtreePrefixes,
6
7
  getChangedSubtrees,
7
8
  hasUncommittedChanges,
8
9
  resolveSubtreePushBranch,
10
+ setLastPushedRef,
9
11
  } from '../git.js';
10
12
  import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
11
13
  import * as ui from '../ui.js';
12
14
 
13
15
  export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
14
- const cwd = process.cwd();
16
+ const cwd = getMonorepoRoot(process.cwd());
15
17
 
16
18
  // ── Preflight ────────────────────────────────────────────────────────────
17
19
  ui.header('Pushing subtrees');
@@ -92,6 +94,8 @@ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
92
94
  ui.pushSlow();
93
95
  try {
94
96
  git(`subtree push --prefix="${target.name}" "${target.name}" "${target.pushBranch}"`, { cwd });
97
+ const headRef = git('rev-parse HEAD', { cwd, silent: true });
98
+ setLastPushedRef(cwd, target.name, headRef);
95
99
  ui.success(`${target.name} pushed`);
96
100
  succeeded++;
97
101
  } catch (err) {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getConfiguredSubtreePushBranch,
3
3
  getCurrentBranch,
4
+ getMonorepoRoot,
4
5
  getSubtreePrefixes,
5
6
  getChangedSubtrees,
6
7
  getTrackedSubtreeBranch,
@@ -31,7 +32,7 @@ export function buildStatusSubtrees({
31
32
  }
32
33
 
33
34
  export async function runStatus({ json }) {
34
- const cwd = process.cwd();
35
+ const cwd = getMonorepoRoot(process.cwd());
35
36
 
36
37
  validateInsideMonorepo(cwd);
37
38
 
package/src/git.js CHANGED
@@ -14,6 +14,10 @@ function getSubtreePushBranchConfigKey(prefixName) {
14
14
  return `unirepo.subtree.${prefixName}.pushBranch`;
15
15
  }
16
16
 
17
+ function getSubtreeLastPushedRefKey(prefixName) {
18
+ return `unirepo.subtree.${prefixName}.lastPushedRef`;
19
+ }
20
+
17
21
  /**
18
22
  * Execute a git command and return trimmed stdout.
19
23
  * @param {string} args - git arguments
@@ -199,15 +203,15 @@ export function getChangedSubtrees(cwd) {
199
203
  continue;
200
204
  }
201
205
 
202
- // Check 2: committed changes since the last subtree add/pull merge
206
+ // Check 2: committed changes since the last push (or last subtree merge as fallback)
203
207
  try {
204
- const mergeCommit = findLastSubtreeMerge(cwd, prefix.name);
205
- if (mergeCommit) {
206
- // Are there any commits after the merge that touch this prefix?
207
- const commits = execSync(
208
- `git log --oneline "${mergeCommit}..HEAD" -- "${prefix.name}"`,
209
- { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
210
- ).trim();
208
+ const lastPushedRef = getLastPushedRef(cwd, prefix.name);
209
+ const anchor = lastPushedRef || findLastSubtreeMerge(cwd, prefix.name);
210
+ if (anchor) {
211
+ const commits = git(
212
+ `log --oneline ${quoteShellArg(anchor + '..HEAD')} -- ${quoteShellArg(prefix.name)}`,
213
+ { cwd, silent: true, allowFailure: true }
214
+ );
211
215
  if (commits) {
212
216
  changed.push(prefix);
213
217
  }
@@ -312,6 +316,43 @@ export function setConfiguredSubtreePushBranch(cwd, prefixName, branch) {
312
316
  );
313
317
  }
314
318
 
319
+ /**
320
+ * Get the SHA of the last HEAD that was successfully pushed for a subtree.
321
+ */
322
+ export function getLastPushedRef(cwd, prefixName) {
323
+ return git(`config --get ${quoteShellArg(getSubtreeLastPushedRefKey(prefixName))}`, {
324
+ cwd,
325
+ silent: true,
326
+ allowFailure: true,
327
+ }) || null;
328
+ }
329
+
330
+ /**
331
+ * Persist the HEAD SHA that was last successfully pushed for a subtree.
332
+ */
333
+ export function setLastPushedRef(cwd, prefixName, ref) {
334
+ git(
335
+ `config ${quoteShellArg(getSubtreeLastPushedRefKey(prefixName))} ${quoteShellArg(ref)}`,
336
+ { cwd, silent: true }
337
+ );
338
+ }
339
+
340
+ /**
341
+ * Resolve the root of the git work tree (the monorepo root), even when called
342
+ * from inside a subtree subdirectory.
343
+ */
344
+ export function getMonorepoRoot(cwd) {
345
+ try {
346
+ return execSync('git rev-parse --show-toplevel', {
347
+ cwd,
348
+ encoding: 'utf-8',
349
+ stdio: ['pipe', 'pipe', 'pipe'],
350
+ }).trim();
351
+ } catch {
352
+ return cwd;
353
+ }
354
+ }
355
+
315
356
  /**
316
357
  * Resolve the effective push/head branch for a subtree.
317
358
  */
package/src/templates.js CHANGED
@@ -21,6 +21,12 @@ This repository is a git-subtree monorepo.
21
21
 
22
22
  ## CLI
23
23
 
24
+ If \`unirepo\` is not installed, install it first:
25
+
26
+ \`\`\`bash
27
+ npm install -g unirepo-cli
28
+ \`\`\`
29
+
24
30
  Preferred when the \`unirepo\` CLI is available:
25
31
 
26
32
  \`\`\`bash
@@ -67,10 +73,15 @@ git subtree pull --prefix=<subtree> <remote-or-url> <branch> --squash
67
73
 
68
74
  3. Make changes inside one or more subtree directories.
69
75
 
70
- 4. Commit in the monorepo. Prefer one commit per subtree unless the change is intentionally coupled.
76
+ 4. Commit in the monorepo. Use one commit per subtree when the changes are independent. When a change is tightly coupled across subtrees (e.g. a shared type change that requires simultaneous updates in both the library and the consumer), commit all affected subtrees together in a single commit so the intent is clear.
71
77
  \`\`\`bash
78
+ # Single subtree
72
79
  git add <subtree>/
73
80
  git commit -m "feat(<subtree>): ..."
81
+
82
+ # Coupled change across subtrees
83
+ git add <subtree-a>/ <subtree-b>/
84
+ git commit -m "feat: ..."
74
85
  \`\`\`
75
86
 
76
87
  5. Inspect what changed before pushing.
@@ -136,4 +147,6 @@ export const GITIGNORE = `.DS_Store
136
147
 
137
148
  *.swp
138
149
  *.swo
150
+
151
+ AGENTS.md
139
152
  `;