unirepo-cli 0.5.3 → 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.3",
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/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