worktree-flow 0.0.1

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/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # worktree-flow
2
+
3
+ Manage git worktrees across a poly-repo environment.
4
+
5
+ ## What is worktree-flow?
6
+
7
+ `flow` helps you work on multi-repo features by creating isolated workspace directories with git worktrees. Instead of switching branches across multiple repositories manually, flow creates a workspace folder containing worktrees for all relevant repos on the same branch.
8
+
9
+ **Before:**
10
+ ```
11
+ ~/repos/
12
+ ├── AGENTS.md
13
+ ├── my-api-1/ (main branch)
14
+ ├── my-api-2/ (main branch)
15
+ └── my-client/ (main branch)
16
+ ```
17
+
18
+ **After running `flow branch TICKET-123`:**
19
+ ```
20
+ ~/workspaces/TICKET-123/
21
+ ├── AGENTS.md (copied from ~/repos)
22
+ ├── my-api-1/ (TICKET-123 branch - new worktree)
23
+ └── my-client/ (TICKET-123 branch - new worktree)
24
+ ```
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install -g worktree-flow
30
+ ```
31
+
32
+ Or install locally:
33
+
34
+ ```bash
35
+ npm install worktree-flow
36
+ npm link
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ Set the paths where your repos live and where workspaces should be created:
42
+
43
+ ```bash
44
+ flow config set source-path ~/repos
45
+ flow config set dest-path ~/workspaces
46
+ ```
47
+
48
+ Optional configuration:
49
+
50
+ ```bash
51
+ # Set the main branch name (default: master)
52
+ flow config set main-branch main
53
+
54
+ # Enable tmux session creation (default: false)
55
+ flow config set tmux true
56
+
57
+ # Set config files to copy to worktrees (default: .env)
58
+ flow config set copy-files .env,.env.local
59
+
60
+ # Set command to run after checkout/branching (optional, prompts if set)
61
+ flow config set post-checkout "npm ci"
62
+ ```
63
+
64
+ Configuration is stored in `~/.config/flow/config.json`.
65
+
66
+ ## Usage
67
+
68
+ ### Create a new branch across repos
69
+
70
+ Interactively select which repos to branch:
71
+
72
+ ```bash
73
+ flow branch TICKET-123
74
+ ```
75
+
76
+ Use spacebar to select repos, enter to confirm. Creates new branches and worktrees.
77
+
78
+ If you've configured a post-checkout command, flow will prompt you to run it in all workspaces in parallel. This is useful for tasks like installing dependencies after branching.
79
+
80
+ ### Checkout an existing branch
81
+
82
+ Automatically detects which repos have the branch:
83
+
84
+ ```bash
85
+ flow checkout TICKET-123
86
+ ```
87
+
88
+ Fetches all repos and creates worktrees for repos that have the branch.
89
+
90
+ If you've configured a post-checkout command, flow will prompt you to run it in all workspaces in parallel. This is useful for tasks like installing dependencies after switching branches.
91
+
92
+ ### Pull changes in a workspace
93
+
94
+ From anywhere inside a workspace directory:
95
+
96
+ ```bash
97
+ cd ~/workspaces/TICKET-123
98
+ flow pull
99
+ ```
100
+
101
+ Pulls latest changes for all repos in the workspace.
102
+
103
+ ### Push changes in a workspace
104
+
105
+ From anywhere inside a workspace directory:
106
+
107
+ ```bash
108
+ cd ~/workspaces/TICKET-123/my-api-1
109
+ flow push
110
+ ```
111
+
112
+ Pushes all repos in the workspace. Automatically sets upstream on first push.
113
+
114
+ ### Check workspace status
115
+
116
+ Check the status of all repos in a workspace:
117
+
118
+ ```bash
119
+ flow status TICKET-123
120
+ ```
121
+
122
+ Fetches latest changes from remote, then shows which repos have:
123
+ - Uncommitted changes
124
+ - Commits ahead of the main branch
125
+ - Are up to date
126
+
127
+ This helps you quickly see what needs attention before removing a workspace.
128
+
129
+ ### Remove a workspace
130
+
131
+ Remove a workspace and all its worktrees:
132
+
133
+ ```bash
134
+ flow remove TICKET-123
135
+ ```
136
+
137
+ This command will:
138
+ 1. Fetch latest changes from remote
139
+ 2. Check all worktrees for uncommitted changes and changes ahead of main (diff against configured main branch)
140
+ 3. Abort if any uncommitted changes or commits ahead of main are found
141
+ 4. Show what will be removed and ask for confirmation (y/n)
142
+ 5. Remove all worktrees from their source repos
143
+ 6. Delete the workspace folder
144
+ 7. Kill the tmux session (if tmux is enabled)
145
+
146
+ The "ahead of main" check compares your branch against the configured main branch (default: `master`) to detect actual code differences, so it's safe to remove branches even after they've been squash-merged and the remote branch deleted.
147
+
148
+ Use this to clean up when you're done with a branch.
149
+
150
+ ## AGENTS.md
151
+
152
+ If an `AGENTS.md` file exists at the root of your source-path, it will be copied to each workspace root.
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Production adapters using real Node.js APIs.
3
+ */
4
+ import fs from 'node:fs';
5
+ import { execFile } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ const execFileAsync = promisify(execFile);
8
+ export class NodeFileSystem {
9
+ existsSync(path) {
10
+ return fs.existsSync(path);
11
+ }
12
+ readFileSync(path, encoding) {
13
+ return fs.readFileSync(path, encoding);
14
+ }
15
+ writeFileSync(path, content) {
16
+ fs.writeFileSync(path, content);
17
+ }
18
+ mkdirSync(path, options) {
19
+ fs.mkdirSync(path, options);
20
+ }
21
+ readdirSync(path, options) {
22
+ return fs.readdirSync(path, options);
23
+ }
24
+ copyFileSync(src, dest) {
25
+ fs.copyFileSync(src, dest);
26
+ }
27
+ rmSync(path, options) {
28
+ fs.rmSync(path, options);
29
+ }
30
+ }
31
+ export class NodeShell {
32
+ async execFile(command, args, options) {
33
+ const result = await execFileAsync(command, args, {
34
+ ...options,
35
+ encoding: options?.encoding || 'utf-8',
36
+ });
37
+ return {
38
+ stdout: result.stdout.trim(),
39
+ stderr: result.stderr.trim(),
40
+ };
41
+ }
42
+ }
43
+ export class NodeConsole {
44
+ log(message) {
45
+ console.log(message);
46
+ }
47
+ error(message) {
48
+ console.error(message);
49
+ }
50
+ write(message) {
51
+ process.stdout.write(message);
52
+ }
53
+ }
54
+ export class NodeProcess {
55
+ exit(code) {
56
+ process.exit(code);
57
+ }
58
+ cwd() {
59
+ return process.cwd();
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Adapter interfaces for I/O operations.
3
+ * These abstractions allow business logic to be tested without real I/O.
4
+ */
5
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerConfigCommand } from './commands/config.js';
4
+ import { registerBranchCommand } from './commands/branch.js';
5
+ import { registerCheckoutCommand } from './commands/checkout.js';
6
+ import { registerListCommand } from './commands/list.js';
7
+ import { registerPullCommand } from './commands/pull.js';
8
+ import { registerPushCommand } from './commands/push.js';
9
+ import { registerRemoveCommand } from './commands/remove.js';
10
+ import { registerStatusCommand } from './commands/status.js';
11
+ import { registerPruneCommand } from './commands/prune.js';
12
+ const program = new Command();
13
+ program
14
+ .name('flow')
15
+ .description('Manage git worktrees across a poly-repo environment')
16
+ .version('0.1.0');
17
+ registerConfigCommand(program);
18
+ registerBranchCommand(program);
19
+ registerCheckoutCommand(program);
20
+ registerListCommand(program);
21
+ registerPullCommand(program);
22
+ registerPushCommand(program);
23
+ registerRemoveCommand(program);
24
+ registerStatusCommand(program);
25
+ registerPruneCommand(program);
26
+ program.parse();
@@ -0,0 +1,76 @@
1
+ import checkbox from '@inquirer/checkbox';
2
+ import input from '@inquirer/input';
3
+ import confirm from '@inquirer/confirm';
4
+ import chalk from 'chalk';
5
+ import { createServices } from '../lib/services.js';
6
+ import { createUseCases } from '../usecases/usecases.js';
7
+ import { NoReposFoundError } from '../lib/errors.js';
8
+ export async function runBranch(branchName, useCases, services, deps) {
9
+ const { sourcePath, destPath } = services.config.getRequired();
10
+ const config = services.config.load();
11
+ const repos = services.repos.discoverRepos(sourcePath);
12
+ if (repos.length === 0) {
13
+ throw new NoReposFoundError(sourcePath);
14
+ }
15
+ // User prompts
16
+ const selected = await deps.checkbox({
17
+ message: `Select repos for branch "${branchName}":`,
18
+ choices: services.repos.formatRepoChoices(repos),
19
+ pageSize: 20,
20
+ });
21
+ if (selected.length === 0) {
22
+ services.console.log('No repos selected.');
23
+ return;
24
+ }
25
+ const sourceBranch = await deps.input({
26
+ message: 'Branch from which branch?',
27
+ default: 'master',
28
+ });
29
+ let shouldRunPostCheckout = false;
30
+ if (config.postCheckout) {
31
+ shouldRunPostCheckout = await deps.confirm({
32
+ message: `Run "${config.postCheckout}" in all workspaces?`,
33
+ default: true,
34
+ });
35
+ }
36
+ services.console.log('\nCreating workspace...');
37
+ // Execute use case
38
+ const result = await useCases.createBranchWorkspace.execute({
39
+ repos: selected,
40
+ branchName,
41
+ sourceBranch,
42
+ sourcePath,
43
+ destPath,
44
+ copyFiles: config.copyFiles,
45
+ tmux: config.tmux,
46
+ postCheckout: shouldRunPostCheckout ? config.postCheckout : undefined,
47
+ });
48
+ // Display results
49
+ services.console.log(`\nCreated workspace at ${chalk.cyan(result.workspacePath)} with ${result.successCount}/${result.totalCount} repos.`);
50
+ if (result.tmuxCreated) {
51
+ services.console.log(`Created tmux session: ${chalk.cyan(branchName)}`);
52
+ }
53
+ if (result.postCheckoutSuccess !== undefined) {
54
+ services.console.log(`\nCompleted post-checkout in ${result.postCheckoutSuccess}/${result.postCheckoutTotal} workspace(s).`);
55
+ }
56
+ else if (!config.postCheckout) {
57
+ services.console.log('\nTip: Configure a post-checkout command to run automatically after branching/checkout.');
58
+ services.console.log(' Example: flow config set post-checkout "npm ci"');
59
+ }
60
+ }
61
+ export function registerBranchCommand(program) {
62
+ program
63
+ .command('branch <branch-name>')
64
+ .description('Create branches and worktrees for selected repos')
65
+ .action(async (branchName) => {
66
+ const services = createServices();
67
+ const useCases = createUseCases(services);
68
+ try {
69
+ await runBranch(branchName, useCases, services, { checkbox, input, confirm });
70
+ }
71
+ catch (error) {
72
+ services.console.error(error.message);
73
+ services.process.exit(1);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,64 @@
1
+ import confirm from '@inquirer/confirm';
2
+ import chalk from 'chalk';
3
+ import { createServices } from '../lib/services.js';
4
+ import { createUseCases } from '../usecases/usecases.js';
5
+ export async function runCheckout(branchName, useCases, services, deps) {
6
+ const { sourcePath, destPath } = services.config.getRequired();
7
+ const config = services.config.load();
8
+ let shouldRunPostCheckout = false;
9
+ if (config.postCheckout) {
10
+ shouldRunPostCheckout = await deps.confirm({
11
+ message: `Run "${config.postCheckout}" in all workspaces?`,
12
+ default: true,
13
+ });
14
+ }
15
+ services.console.log('\nChecking for branch...');
16
+ try {
17
+ // Execute use case
18
+ const result = await useCases.checkoutWorkspace.execute({
19
+ branchName,
20
+ sourcePath,
21
+ destPath,
22
+ copyFiles: config.copyFiles,
23
+ tmux: config.tmux,
24
+ postCheckout: shouldRunPostCheckout ? config.postCheckout : undefined,
25
+ });
26
+ // Display branch check results
27
+ for (const checkResult of result.branchCheckResults) {
28
+ if (checkResult.error) {
29
+ services.console.log(`${checkResult.repoName}... ${chalk.red(`error: ${checkResult.error}`)}`);
30
+ }
31
+ else if (checkResult.hasBranch) {
32
+ services.console.log(`${checkResult.repoName}... ${chalk.green('found')}`);
33
+ }
34
+ else {
35
+ services.console.log(`${checkResult.repoName}... ${chalk.dim('no branch')}`);
36
+ }
37
+ }
38
+ services.console.log(`\nCreated workspace at ${chalk.cyan(result.workspacePath)} with ${result.successCount}/${result.totalCount} repos.`);
39
+ if (result.tmuxCreated) {
40
+ services.console.log(`Created tmux session: ${chalk.cyan(branchName)}`);
41
+ }
42
+ if (result.postCheckoutSuccess !== undefined) {
43
+ services.console.log(`\nCompleted post-checkout in ${result.postCheckoutSuccess}/${result.postCheckoutTotal} workspace(s).`);
44
+ }
45
+ else if (!config.postCheckout) {
46
+ services.console.log('\nTip: Configure a post-checkout command to run automatically after branching/checkout.');
47
+ services.console.log(' Example: flow config set post-checkout "npm ci"');
48
+ }
49
+ }
50
+ catch (error) {
51
+ services.console.error(error.message);
52
+ services.process.exit(1);
53
+ }
54
+ }
55
+ export function registerCheckoutCommand(program) {
56
+ program
57
+ .command('checkout <branch-name>')
58
+ .description('Checkout an existing branch across repos')
59
+ .action(async (branchName) => {
60
+ const services = createServices();
61
+ const useCases = createUseCases(services);
62
+ await runCheckout(branchName, useCases, services, { confirm });
63
+ });
64
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { z } from 'zod';
3
+ import { isValidKey, validateAndTransformConfigValue, CONFIG_KEYS, } from '../lib/config.js';
4
+ import { createServices } from '../lib/services.js';
5
+ export function registerConfigCommand(program) {
6
+ const configCmd = program
7
+ .command('config')
8
+ .description('Manage flow configuration');
9
+ configCmd
10
+ .command('set <key> <value>')
11
+ .description('Set a config value (source-path, dest-path, copy-files, tmux, main-branch, post-checkout)')
12
+ .action((key, value) => {
13
+ const services = createServices();
14
+ if (!isValidKey(key)) {
15
+ services.console.error(`Unknown config key: ${key}\nValid keys: ${CONFIG_KEYS.join(', ')}`);
16
+ services.process.exit(1);
17
+ }
18
+ try {
19
+ const config = services.config.loadRaw();
20
+ const transformedValue = validateAndTransformConfigValue(key, value);
21
+ config[key] = transformedValue;
22
+ services.config.saveRaw(config);
23
+ services.console.log(chalk.green(`Set ${key} = ${transformedValue}`));
24
+ }
25
+ catch (error) {
26
+ if (error instanceof z.ZodError) {
27
+ services.console.error(`Invalid value for ${key}: ${error.issues[0].message}`);
28
+ }
29
+ else {
30
+ services.console.error(`Error setting config: ${error}`);
31
+ }
32
+ services.process.exit(1);
33
+ }
34
+ });
35
+ configCmd
36
+ .command('list')
37
+ .description('List all config options and their current values')
38
+ .action(() => {
39
+ const services = createServices();
40
+ services.console.log(chalk.bold('\nCurrent configuration:'));
41
+ services.console.log('');
42
+ const displayConfig = services.config.getDisplayConfig();
43
+ for (const key of CONFIG_KEYS) {
44
+ const { value, isDefault } = displayConfig[key];
45
+ const displayValue = isDefault ? chalk.gray(value) : chalk.green(value);
46
+ services.console.log(` ${chalk.cyan(key)}: ${displayValue}`);
47
+ }
48
+ services.console.log('');
49
+ });
50
+ }
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import { createServices } from '../lib/services.js';
3
+ export async function runList(services) {
4
+ const { destPath } = services.config.getRequired();
5
+ const workspaces = services.workspaceDir.listWorkspaces(destPath);
6
+ if (workspaces.length === 0) {
7
+ services.console.log('No workspaces found.');
8
+ return;
9
+ }
10
+ services.console.log(chalk.bold('\nWorkspaces:'));
11
+ for (const workspace of workspaces) {
12
+ services.console.log(` ${chalk.cyan(workspace.name)} ${chalk.dim(`(${workspace.repoCount} repo${workspace.repoCount === 1 ? '' : 's'})`)}`);
13
+ }
14
+ services.console.log('');
15
+ }
16
+ export function registerListCommand(program) {
17
+ program
18
+ .command('list')
19
+ .description('List all workspaces in the target directory')
20
+ .action(async () => {
21
+ const services = createServices();
22
+ try {
23
+ await runList(services);
24
+ }
25
+ catch (error) {
26
+ services.console.error(error.message);
27
+ services.process.exit(1);
28
+ }
29
+ });
30
+ }
@@ -0,0 +1,93 @@
1
+ import chalk from 'chalk';
2
+ import confirm from '@inquirer/confirm';
3
+ import { createServices } from '../lib/services.js';
4
+ import { createUseCases } from '../usecases/usecases.js';
5
+ export async function runPrune(useCases, services, deps) {
6
+ const { sourcePath, destPath } = services.config.getRequired();
7
+ const config = services.config.load();
8
+ services.console.log('Analyzing workspaces for pruning...\n');
9
+ // Analyze workspaces to find prunable ones
10
+ const result = await useCases.discoverPrunableWorkspaces.execute({
11
+ destPath,
12
+ sourcePath,
13
+ mainBranch: config.mainBranch,
14
+ daysOld: 7,
15
+ });
16
+ if (result.prunable.length === 0) {
17
+ services.console.log('No workspaces to prune.');
18
+ services.console.log(`\nWorkspaces are prunable when:`);
19
+ services.console.log(` - All worktrees have no uncommitted changes`);
20
+ services.console.log(` - All worktrees are not ahead of ${config.mainBranch}`);
21
+ services.console.log(` - Last commit is more than 7 days old`);
22
+ services.process.exit(0);
23
+ }
24
+ // Display prunable workspaces
25
+ services.console.log(`${chalk.yellow('Found')} ${chalk.cyan(result.prunable.length)} ${chalk.yellow('workspace(s) that can be pruned:')}\n`);
26
+ for (const workspace of result.prunable) {
27
+ const daysOld = Math.floor((Date.now() - workspace.oldestCommitDate.getTime()) / (1000 * 60 * 60 * 24));
28
+ services.console.log(` ${chalk.cyan(workspace.name)}`);
29
+ services.console.log(` Repos: ${workspace.repoCount}`);
30
+ services.console.log(` Last commit: ${daysOld} days ago`);
31
+ services.console.log(` Status: ${chalk.green('clean')}`);
32
+ services.console.log('');
33
+ }
34
+ // Summary
35
+ const totalRepos = result.prunable.reduce((sum, ws) => sum + ws.repoCount, 0);
36
+ services.console.log(`${chalk.yellow('This will remove:')}`);
37
+ services.console.log(` ${result.prunable.length} workspace(s)`);
38
+ services.console.log(` ${totalRepos} worktree(s)`);
39
+ const confirmed = await deps.confirm({
40
+ message: 'Are you sure you want to prune these workspaces?',
41
+ default: false,
42
+ });
43
+ if (!confirmed) {
44
+ services.console.log('\nCancelled.');
45
+ services.process.exit(0);
46
+ }
47
+ // Remove each workspace
48
+ services.console.log('\nPruning workspaces...\n');
49
+ let successCount = 0;
50
+ let errorCount = 0;
51
+ for (const workspace of result.prunable) {
52
+ try {
53
+ services.console.log(`Removing ${chalk.cyan(workspace.name)}...`);
54
+ await useCases.removeWorkspace.execute({
55
+ workspacePath: workspace.path,
56
+ branchName: workspace.name,
57
+ sourcePath,
58
+ mainBranch: config.mainBranch,
59
+ tmux: config.tmux,
60
+ });
61
+ services.console.log(` ${chalk.green('✓')} Removed ${workspace.name}`);
62
+ successCount++;
63
+ }
64
+ catch (error) {
65
+ services.console.log(` ${chalk.red('✗')} Failed to remove ${workspace.name}: ${error.message}`);
66
+ errorCount++;
67
+ }
68
+ }
69
+ // Display summary
70
+ services.console.log('');
71
+ if (successCount > 0) {
72
+ services.console.log(`${chalk.green('Successfully pruned')} ${successCount} workspace(s)`);
73
+ }
74
+ if (errorCount > 0) {
75
+ services.console.log(`${chalk.red('Failed to prune')} ${errorCount} workspace(s)`);
76
+ }
77
+ }
78
+ export function registerPruneCommand(program) {
79
+ program
80
+ .command('prune')
81
+ .description('Remove old workspaces with no uncommitted changes and commits older than 7 days')
82
+ .action(async () => {
83
+ const services = createServices();
84
+ const useCases = createUseCases(services);
85
+ try {
86
+ await runPrune(useCases, services, { confirm });
87
+ }
88
+ catch (error) {
89
+ services.console.error(error.message);
90
+ services.process.exit(1);
91
+ }
92
+ });
93
+ }
@@ -0,0 +1,31 @@
1
+ import chalk from 'chalk';
2
+ import { createServices } from '../lib/services.js';
3
+ import { createUseCases } from '../usecases/usecases.js';
4
+ import { resolveWorkspace } from '../lib/workspaceResolver.js';
5
+ export async function runPull(branchName, useCases, services) {
6
+ const { workspacePath } = resolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
7
+ const dirs = services.workspaceDir.getWorktreeDirs(workspacePath);
8
+ if (dirs.length === 0) {
9
+ services.console.error('No repos found in workspace.');
10
+ services.process.exit(1);
11
+ }
12
+ services.console.log(`Pulling ${dirs.length} repo(s) in ${chalk.cyan(workspacePath)}...\n`);
13
+ const result = await useCases.pullWorkspace.execute({ workspacePath });
14
+ services.console.log(`\n${result.successCount}/${result.totalCount} repos pulled successfully.`);
15
+ }
16
+ export function registerPullCommand(program) {
17
+ program
18
+ .command('pull [branch-name]')
19
+ .description('Pull all repos in a workspace (auto-detects from current directory if branch not provided)')
20
+ .action(async (branchName) => {
21
+ const services = createServices();
22
+ const useCases = createUseCases(services);
23
+ try {
24
+ await runPull(branchName, useCases, services);
25
+ }
26
+ catch (error) {
27
+ services.console.error(error.message);
28
+ services.process.exit(1);
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,31 @@
1
+ import chalk from 'chalk';
2
+ import { createServices } from '../lib/services.js';
3
+ import { createUseCases } from '../usecases/usecases.js';
4
+ import { resolveWorkspace } from '../lib/workspaceResolver.js';
5
+ export async function runPush(branchName, useCases, services) {
6
+ const { workspacePath } = resolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
7
+ const dirs = services.workspaceDir.getWorktreeDirs(workspacePath);
8
+ if (dirs.length === 0) {
9
+ services.console.error('No repos found in workspace.');
10
+ services.process.exit(1);
11
+ }
12
+ services.console.log(`Pushing ${dirs.length} repo(s) in ${chalk.cyan(workspacePath)}...\n`);
13
+ const result = await useCases.pushWorkspace.execute({ workspacePath });
14
+ services.console.log(`\n${result.successCount}/${result.totalCount} repos pushed successfully.`);
15
+ }
16
+ export function registerPushCommand(program) {
17
+ program
18
+ .command('push [branch-name]')
19
+ .description('Push all repos in a workspace (auto-detects from current directory if branch not provided)')
20
+ .action(async (branchName) => {
21
+ const services = createServices();
22
+ const useCases = createUseCases(services);
23
+ try {
24
+ await runPush(branchName, useCases, services);
25
+ }
26
+ catch (error) {
27
+ services.console.error(error.message);
28
+ services.process.exit(1);
29
+ }
30
+ });
31
+ }