unirepo-cli 0.5.2 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unirepo-cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "CLI tool for creating and managing git-subtree monorepos — run your agents across repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,104 @@
1
+ import {
2
+ git,
3
+ getMonorepoRoot,
4
+ getRemoteBranch,
5
+ getSubtreePrefixes,
6
+ setConfiguredSubtreeBranch,
7
+ unsetConfiguredSubtreePushBranch,
8
+ setLastPushedRef,
9
+ hasUncommittedChanges,
10
+ } from '../git.js';
11
+ import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
12
+ import * as ui from '../ui.js';
13
+
14
+ export async function runReset({ subtrees: requestedSubtrees, branch, dryRun }) {
15
+ const cwd = getMonorepoRoot(process.cwd());
16
+
17
+ ui.header('Resetting subtrees');
18
+ ui.blank();
19
+
20
+ validateGitSubtree();
21
+ validateInsideMonorepo(cwd);
22
+
23
+ if (hasUncommittedChanges(cwd)) {
24
+ ui.error('You have uncommitted changes. Commit or stash before resetting.');
25
+ ui.blank();
26
+ process.exit(1);
27
+ }
28
+
29
+ const allPrefixes = getSubtreePrefixes(cwd);
30
+
31
+ let prefixes;
32
+ if (requestedSubtrees && requestedSubtrees.length > 0) {
33
+ const names = new Set(allPrefixes.map((p) => p.name));
34
+ for (const name of requestedSubtrees) {
35
+ if (!names.has(name)) {
36
+ throw new Error(`"${name}" is not a tracked subtree. Run "unirepo status" to see available subtrees.`);
37
+ }
38
+ }
39
+ prefixes = allPrefixes.filter((p) => requestedSubtrees.includes(p.name));
40
+ } else {
41
+ prefixes = allPrefixes;
42
+ }
43
+
44
+ if (prefixes.length === 0) {
45
+ ui.info('No tracked subtrees found. Nothing to reset.');
46
+ ui.blank();
47
+ return;
48
+ }
49
+
50
+ // Use the remote's actual default branch — not the stored config which may be a stale feature branch.
51
+ const targets = prefixes.map((p) => ({
52
+ ...p,
53
+ upstreamBranch: branch || getRemoteBranch(cwd, p.name) || 'main',
54
+ }));
55
+
56
+ const uniqueBranches = [...new Set(targets.map((t) => t.upstreamBranch))];
57
+ if (uniqueBranches.length === 1) {
58
+ ui.info(`Resetting all subtrees to branch: ${uniqueBranches[0]}`);
59
+ } else {
60
+ ui.info('Resetting subtrees to their upstream default branches');
61
+ }
62
+ ui.blank();
63
+
64
+ if (dryRun) {
65
+ const commands = targets.flatMap((t) => [
66
+ `git subtree pull --squash --prefix="${t.name}" "${t.name}" "${t.upstreamBranch}"`,
67
+ `git config unirepo.subtree.${t.name}.branch "${t.upstreamBranch}"`,
68
+ `git config --unset unirepo.subtree.${t.name}.pushBranch`,
69
+ ]);
70
+ ui.dryRun(commands);
71
+ return;
72
+ }
73
+
74
+ let succeeded = 0;
75
+ let failed = 0;
76
+
77
+ for (let i = 0; i < targets.length; i++) {
78
+ const target = targets[i];
79
+ ui.repoStep(i + 1, targets.length, target.name, 'Resetting');
80
+ ui.repoDetail('Branch', target.upstreamBranch);
81
+
82
+ try {
83
+ git(`subtree pull --squash --prefix="${target.name}" "${target.name}" "${target.upstreamBranch}"`, { cwd });
84
+ const headRef = git('rev-parse HEAD', { cwd, silent: true });
85
+ setConfiguredSubtreeBranch(cwd, target.name, target.upstreamBranch);
86
+ unsetConfiguredSubtreePushBranch(cwd, target.name);
87
+ setLastPushedRef(cwd, target.name, headRef);
88
+ ui.success(`${target.name} reset to ${target.upstreamBranch}`);
89
+ succeeded++;
90
+ } catch (err) {
91
+ ui.error(`Failed to reset ${target.name}: ${err.message}`);
92
+ failed++;
93
+ }
94
+
95
+ ui.blank();
96
+ }
97
+
98
+ if (failed === 0) {
99
+ ui.success(`All ${succeeded} subtree(s) reset`);
100
+ } else {
101
+ ui.warning(`${succeeded} reset, ${failed} failed`);
102
+ }
103
+ ui.blank();
104
+ }
package/src/git.js CHANGED
@@ -316,6 +316,17 @@ export function setConfiguredSubtreePushBranch(cwd, prefixName, branch) {
316
316
  );
317
317
  }
318
318
 
319
+ /**
320
+ * Remove the push/head branch override for a subtree from local git config.
321
+ */
322
+ export function unsetConfiguredSubtreePushBranch(cwd, prefixName) {
323
+ git(`config --unset ${quoteShellArg(getSubtreePushBranchConfigKey(prefixName))}`, {
324
+ cwd,
325
+ silent: true,
326
+ allowFailure: true,
327
+ });
328
+ }
329
+
319
330
  /**
320
331
  * Get the SHA of the last HEAD that was successfully pushed for a subtree.
321
332
  */
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ import { runStatus } from './commands/status.js';
9
9
  import { runPush } from './commands/push.js';
10
10
  import { runBranch } from './commands/branch.js';
11
11
  import { runPr } from './commands/pr.js';
12
+ import { runReset } from './commands/reset.js';
12
13
  import { pathToFileURL } from 'node:url';
13
14
  import { realpathSync } from 'node:fs';
14
15
 
@@ -73,6 +74,7 @@ const COMMAND_USAGE = {
73
74
  push: 'Usage: unirepo push [subtree...] [--branch <name>] [--dry-run]',
74
75
  branch: 'Usage: unirepo branch [name]',
75
76
  pr: 'Usage: unirepo pr [subtree...] --title <title> [--body <text>] [--base <name>] [--head <name>] [--draft] [--dry-run]',
77
+ reset: 'Usage: unirepo reset [subtree...] [--branch <name>] [--dry-run]',
76
78
  };
77
79
 
78
80
  const COMMAND_FLAGS = {
@@ -84,6 +86,7 @@ const COMMAND_FLAGS = {
84
86
  push: new Set(['branch', 'dryRun']),
85
87
  branch: new Set(),
86
88
  pr: new Set(['title', 'body', 'base', 'head', 'draft', 'dryRun']),
89
+ reset: new Set(['branch', 'dryRun']),
87
90
  };
88
91
 
89
92
  const FLAG_NAMES = {
@@ -227,6 +230,15 @@ export async function main() {
227
230
  break;
228
231
  }
229
232
 
233
+ case 'reset': {
234
+ await runReset({
235
+ subtrees: positional.length > 0 ? positional : undefined,
236
+ branch: flags.branch,
237
+ dryRun: flags.dryRun || false,
238
+ });
239
+ break;
240
+ }
241
+
230
242
  default:
231
243
  ui.error(`Unknown command: ${command}`);
232
244
  ui.blank();
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
  `;
package/src/ui.js CHANGED
@@ -181,6 +181,7 @@ ${chalk.bold('Commands:')}
181
181
  ${chalk.green('push')} [subtree...] Push changed subtrees upstream
182
182
  ${chalk.green('pr')} [subtree...] Open PRs for changed subtree repos
183
183
  ${chalk.green('branch')} [name] Create or show the workspace branch
184
+ ${chalk.green('reset')} [subtree...] Pull from upstream main and align all push branches
184
185
  ${chalk.green('version')} Show the installed CLI version
185
186
 
186
187
  ${chalk.bold('Global options:')}
@@ -207,6 +208,10 @@ ${chalk.bold('Push options:')}
207
208
  --branch <name> Branch name for all selected subtree pushes
208
209
  --dry-run Show commands without executing
209
210
 
211
+ ${chalk.bold('Reset options:')}
212
+ --branch <name> Branch to pull from and align push targets to (default: each subtree's upstream default)
213
+ --dry-run Show commands without executing
214
+
210
215
  ${chalk.bold('PR options:')}
211
216
  --title <title> Shared PR title (required)
212
217
  --body <text> Shared PR description
@@ -238,6 +243,9 @@ ${chalk.bold('Examples:')}
238
243
  ${chalk.dim('# Override one subtree push/PR branch via local git config')}
239
244
  git config unirepo.subtree.api.pushBranch feature-api
240
245
 
246
+ ${chalk.dim('# After merging a feature, reset all subtrees back to main')}
247
+ npx unirepo reset
248
+
241
249
  ${chalk.dim('# Push changes upstream')}
242
250
  npx unirepo push --dry-run
243
251