worktree-flow 0.0.16 → 0.0.18

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 CHANGED
@@ -26,7 +26,7 @@ npm install -g worktree-flow
26
26
  flow quickstart
27
27
 
28
28
  # Create a workspace with new branches
29
- flow branch TICKET-123
29
+ flow create TICKET-123
30
30
  # → Select repos interactively, creates branches + worktrees
31
31
 
32
32
  # Or checkout an existing branch
@@ -48,7 +48,7 @@ Working on features that span multiple repositories means manually creating bran
48
48
 
49
49
  ## Commands
50
50
 
51
- ### `flow branch <name>`
51
+ ### `flow create <name>`
52
52
 
53
53
  Create a new branch across selected repos. Interactively select which repos to include, then creates branches and worktrees in a new workspace directory.
54
54
 
@@ -56,6 +56,10 @@ Create a new branch across selected repos. Interactively select which repos to i
56
56
 
57
57
  Checkout an existing branch. Fetches all repos, detects which have the branch, and creates worktrees.
58
58
 
59
+ ### `flow attach [name]`
60
+
61
+ Attach repos to an existing workspace. Discovers available repos not yet in the workspace, presents an interactive picker, creates worktrees with new branches, copies config files, and runs post-checkout commands. Auto-detects the workspace from the current directory, or specify a branch name explicitly.
62
+
59
63
  ### `flow pull`
60
64
 
61
65
  Pull latest changes for all repos in the current workspace. Run from anywhere inside a workspace.
@@ -77,15 +81,19 @@ List all workspaces with status indicators. Shows:
77
81
 
78
82
  Fetches all repos before checking status to ensure accurate information. The status check uses git's patch-id comparison, which correctly handles squash-merged branches.
79
83
 
80
- ### `flow remove <name>`
84
+ ### `flow drop <name>`
81
85
 
82
- Remove a workspace and all its worktrees. Fetches latest, checks for uncommitted changes, and prompts for confirmation before removing. Committed changes are safe to remove since they're preserved in git history.
86
+ Drop a workspace and all its worktrees. Fetches latest, checks for uncommitted changes, and prompts for confirmation before removing. Committed changes are safe to drop since they're preserved in git history.
83
87
 
84
- ### `flow prune` (alias: `clean`)
88
+ ### `flow prune`
85
89
 
86
90
  Remove workspaces interactively. Shows all workspaces with their status (similar to `flow list`) and lets you select which ones to prune. Only workspaces with no uncommitted changes can be successfully removed.
87
91
 
88
- ### `flow tmux resume`
92
+ ### `flow fetch [name]`
93
+
94
+ Fetch repos for a specific workspace (when branch name provided), or fetch all repos used across all workspaces (when no branch given). Always bypasses the fetch cache.
95
+
96
+ ### `flow tmux sync`
89
97
 
90
98
  Create tmux sessions for all workspaces that don't already have one. Each session is created with split panes (one for the workspace root and one for each worktree that matches a repo from source-path) using a tiled layout. Skips workspaces that already have active sessions.
91
99
 
@@ -126,6 +134,22 @@ Repos with per-repo commands use those; others fall back to the global `post-che
126
134
 
127
135
  When tmux is enabled, post-checkout commands run in the corresponding tmux panes instead of executing directly. This lets you see the output in real-time within your tmux session.
128
136
 
137
+ ### Repo-level configuration
138
+
139
+ Individual repos can define a `flow-config.json` at their root to override global settings:
140
+
141
+ ```json
142
+ {
143
+ "copy-files": ".env,.env.local",
144
+ "post-checkout": "yarn install"
145
+ }
146
+ ```
147
+
148
+ Both fields are optional. Precedence (highest to lowest):
149
+
150
+ - **post-checkout**: repo's `flow-config.json` > global `post-checkout`
151
+ - **copy-files**: repo's `flow-config.json` > global `copy-files`
152
+
129
153
  ## AGENTS.md
130
154
 
131
155
  If an `AGENTS.md` file exists at the root of your source-path, it will be copied into each workspace. This is useful for providing AI coding agents with context about your multi-repo setup.
package/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { registerConfigCommand } from './commands/config.js';
4
+ import { registerCreateCommand } from './commands/create.js';
5
+ import { registerAttachCommand } from './commands/attach.js';
4
6
  import { registerBranchCommand } from './commands/branch.js';
5
7
  import { registerCheckoutCommand } from './commands/checkout.js';
6
8
  import { registerListCommand } from './commands/list.js';
7
9
  import { registerPullCommand } from './commands/pull.js';
8
10
  import { registerPushCommand } from './commands/push.js';
9
- import { registerRemoveCommand } from './commands/remove.js';
11
+ import { registerDropCommand } from './commands/drop.js';
10
12
  import { registerStatusCommand } from './commands/status.js';
11
13
  import { registerPruneCommand } from './commands/prune.js';
12
14
  import { registerFetchCommand } from './commands/fetch.js';
@@ -18,12 +20,14 @@ program
18
20
  .description('Manage git worktrees across a poly-repo environment')
19
21
  .version('0.1.0');
20
22
  registerConfigCommand(program);
23
+ registerCreateCommand(program);
24
+ registerAttachCommand(program);
21
25
  registerBranchCommand(program);
22
26
  registerCheckoutCommand(program);
23
27
  registerListCommand(program);
24
28
  registerPullCommand(program);
25
29
  registerPushCommand(program);
26
- registerRemoveCommand(program);
30
+ registerDropCommand(program);
27
31
  registerStatusCommand(program);
28
32
  registerPruneCommand(program);
29
33
  registerFetchCommand(program);
@@ -0,0 +1,120 @@
1
+ import path from 'node:path';
2
+ import checkbox, { Separator } from '@inquirer/checkbox';
3
+ import input from '@inquirer/input';
4
+ import confirm from '@inquirer/confirm';
5
+ import chalk from 'chalk';
6
+ import { createServices } from '../lib/services.js';
7
+ import { createUseCases } from '../usecases/usecases.js';
8
+ import { NoReposFoundError } from '../lib/errors.js';
9
+ import { resolveWorkspace } from '../lib/workspaceResolver.js';
10
+ import { buildRepoCheckboxChoices } from './helpers.js';
11
+ export async function runAttach(branchName, useCases, services, deps) {
12
+ // 1. Resolve workspace (from arg or cwd)
13
+ const { workspacePath, displayName } = resolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
14
+ const { sourcePath } = services.config.getRequired();
15
+ const config = services.config.load();
16
+ // 2. Discover all repos
17
+ const repos = services.repos.discoverRepos(sourcePath);
18
+ if (repos.length === 0) {
19
+ throw new NoReposFoundError(sourcePath);
20
+ }
21
+ // 3. Filter out repos already in the workspace
22
+ const existingWorktrees = services.workspaceDir
23
+ .getWorktreeDirs(workspacePath)
24
+ .map((dir) => path.basename(dir));
25
+ const existingSet = new Set(existingWorktrees);
26
+ const availableRepos = repos.filter((repoPath) => !existingSet.has(path.basename(repoPath)));
27
+ if (availableRepos.length === 0) {
28
+ services.console.log('All repos are already in this workspace.');
29
+ return;
30
+ }
31
+ // 4. Repo picker (same pattern as branch command)
32
+ const checkboxChoices = buildRepoCheckboxChoices(availableRepos, services, [], (label) => new Separator(label));
33
+ const selected = await deps.checkbox({
34
+ message: `Select repos to attach to "${displayName}":`,
35
+ choices: checkboxChoices,
36
+ pageSize: 20,
37
+ loop: false,
38
+ });
39
+ if (selected.length === 0) {
40
+ services.console.log('No repos selected.');
41
+ return;
42
+ }
43
+ // 5. Ask for source branch
44
+ const sourceBranch = await deps.input({
45
+ message: 'Branch from which branch?',
46
+ default: 'master',
47
+ });
48
+ // 6. Post-checkout confirmation
49
+ let shouldRunPostCheckout = false;
50
+ if (config.postCheckout) {
51
+ shouldRunPostCheckout = await deps.confirm({
52
+ message: `Run "${config.postCheckout}" in new workspaces?`,
53
+ default: true,
54
+ });
55
+ }
56
+ services.console.log('\nAttaching repos to workspace...');
57
+ // 7. Fetch selected repos
58
+ await services.fetch.fetchRepos(selected, {
59
+ ttlSeconds: config.fetchCacheTtlSeconds,
60
+ });
61
+ // 8. For each selected repo in parallel: createBranch then addToWorkspace
62
+ // If tmux is enabled, use the workspace branch name as session name
63
+ const sessionName = config.tmux ? displayName : undefined;
64
+ const results = await Promise.allSettled(selected.map(async (repoPath) => {
65
+ const repoName = path.basename(repoPath);
66
+ // Resolve per-repo post-checkout command
67
+ const repoConf = services.repoConfig.load(repoPath);
68
+ const resolvedPostCheckout = shouldRunPostCheckout
69
+ ? services.repoConfig.resolvePostCheckout(repoName, config.perRepoPostCheckout, repoConf, config.postCheckout)
70
+ : undefined;
71
+ // a. Create branch in source repo
72
+ const branchResult = await useCases.createBranch.execute({
73
+ repoPath,
74
+ branchName: displayName,
75
+ sourceBranch,
76
+ });
77
+ // b. Add repo to workspace (creates worktree, copies files, tmux pane, post-checkout)
78
+ return useCases.addToWorkspace.execute({
79
+ repoPath,
80
+ workspacePath,
81
+ branchName: displayName,
82
+ baseBranch: branchResult.baseBranch,
83
+ sessionName,
84
+ copyFiles: config.copyFiles,
85
+ postCheckout: resolvedPostCheckout,
86
+ });
87
+ }));
88
+ // 9. Track usage
89
+ services.fetchCache.trackBranchUsage(selected.map((r) => path.basename(r)));
90
+ // Tally results
91
+ const successCount = results.filter((r) => r.status === 'fulfilled').length;
92
+ const totalCount = results.length;
93
+ const postCheckoutResults = results
94
+ .filter((r) => r.status === 'fulfilled')
95
+ .map((r) => r.value)
96
+ .filter((r) => r.postCheckoutRan);
97
+ const postCheckoutSuccess = postCheckoutResults.filter((r) => r.postCheckoutSuccess).length;
98
+ const postCheckoutTotal = postCheckoutResults.length;
99
+ // 10. Display results
100
+ services.console.log(`\nAttached ${successCount}/${totalCount} repos to ${chalk.cyan(workspacePath)}.`);
101
+ if (postCheckoutTotal > 0) {
102
+ services.console.log(`\nCompleted post-checkout in ${postCheckoutSuccess}/${postCheckoutTotal} workspace(s).`);
103
+ }
104
+ }
105
+ export function registerAttachCommand(program) {
106
+ program
107
+ .command('attach [branch-name]')
108
+ .description('Attach repos to an existing workspace (auto-detects from current directory if branch not provided)')
109
+ .action(async (branchName) => {
110
+ const services = createServices();
111
+ const useCases = createUseCases(services);
112
+ try {
113
+ await runAttach(branchName, useCases, services, { checkbox, input, confirm });
114
+ }
115
+ catch (error) {
116
+ services.console.error(error.message);
117
+ services.process.exit(1);
118
+ }
119
+ });
120
+ }
@@ -1,104 +1,11 @@
1
- import path from 'node:path';
2
- import checkbox, { Separator } from '@inquirer/checkbox';
3
- import input from '@inquirer/input';
4
- import confirm from '@inquirer/confirm';
5
1
  import chalk from 'chalk';
6
2
  import { createServices } from '../lib/services.js';
7
- import { createUseCases } from '../usecases/usecases.js';
8
- import { NoReposFoundError } from '../lib/errors.js';
9
- export async function runBranch(branchName, useCases, services, deps) {
10
- const { sourcePath, destPath } = services.config.getRequired();
11
- const config = services.config.load();
12
- const repos = services.repos.discoverRepos(sourcePath);
13
- if (repos.length === 0) {
14
- throw new NoReposFoundError(sourcePath);
15
- }
16
- // User prompts
17
- const choices = services.repos.formatRepoChoices(repos).map((choice) => ({
18
- ...choice,
19
- checked: config.branchAutoSelectRepos.includes(choice.name),
20
- }));
21
- const recentlyUsed = new Set(services.fetchCache.getRecentlyUsedRepos(8));
22
- const commonlyUsed = choices.filter((c) => recentlyUsed.has(c.name));
23
- let checkboxChoices;
24
- if (commonlyUsed.length > 0) {
25
- const commonlyUsedNames = new Set(commonlyUsed.map((c) => c.name));
26
- const remaining = choices.filter((c) => !commonlyUsedNames.has(c.name));
27
- checkboxChoices = [
28
- new Separator('Recently Used'),
29
- ...commonlyUsed,
30
- ...(remaining.length > 0 ? [new Separator(), ...remaining] : []),
31
- ];
32
- }
33
- else {
34
- checkboxChoices = choices;
35
- }
36
- const selected = await deps.checkbox({
37
- message: `Select repos for branch "${branchName}":`,
38
- choices: checkboxChoices,
39
- pageSize: 20,
40
- loop: false,
41
- });
42
- if (selected.length === 0) {
43
- services.console.log('No repos selected.');
44
- return;
45
- }
46
- const sourceBranch = await deps.input({
47
- message: 'Branch from which branch?',
48
- default: 'master',
49
- });
50
- let shouldRunPostCheckout = false;
51
- if (config.postCheckout) {
52
- shouldRunPostCheckout = await deps.confirm({
53
- message: `Run "${config.postCheckout}" in all workspaces?`,
54
- default: true,
55
- });
56
- }
57
- services.console.log('\nCreating workspace...');
58
- // Fetch all selected repos
59
- await services.fetch.fetchRepos(selected, {
60
- ttlSeconds: config.fetchCacheTtlSeconds,
61
- });
62
- // Execute use case
63
- const result = await useCases.createBranchWorkspace.execute({
64
- repos: selected,
65
- branchName,
66
- sourceBranch,
67
- sourcePath,
68
- destPath,
69
- copyFiles: config.copyFiles,
70
- tmux: config.tmux,
71
- postCheckout: shouldRunPostCheckout ? config.postCheckout : undefined,
72
- perRepoPostCheckout: shouldRunPostCheckout ? config.perRepoPostCheckout : {},
73
- });
74
- // Track which repos were branched from
75
- services.fetchCache.trackBranchUsage(selected.map((r) => path.basename(r)));
76
- // Display results
77
- services.console.log(`\nCreated workspace at ${chalk.cyan(result.workspacePath)} with ${result.successCount}/${result.totalCount} repos.`);
78
- if (result.tmuxCreated) {
79
- services.console.log(`Created tmux session: ${chalk.cyan(branchName)}`);
80
- }
81
- if (result.postCheckoutSuccess !== undefined) {
82
- services.console.log(`\nCompleted post-checkout in ${result.postCheckoutSuccess}/${result.postCheckoutTotal} workspace(s).`);
83
- }
84
- else if (!config.postCheckout) {
85
- services.console.log('\nTip: Configure a post-checkout command to run automatically after branching/checkout.');
86
- services.console.log(' Example: flow config set post-checkout "npm ci"');
87
- }
88
- }
89
3
  export function registerBranchCommand(program) {
90
4
  program
91
5
  .command('branch <branch-name>')
92
- .description('Create branches and worktrees for selected repos')
93
- .action(async (branchName) => {
6
+ .description(chalk.dim('Deprecated: use "flow create <branch-name>" instead'))
7
+ .action(async () => {
94
8
  const services = createServices();
95
- const useCases = createUseCases(services);
96
- try {
97
- await runBranch(branchName, useCases, services, { checkbox, input, confirm });
98
- }
99
- catch (error) {
100
- services.console.error(error.message);
101
- services.process.exit(1);
102
- }
9
+ services.console.log(chalk.yellow('⚠ "flow branch" is deprecated. Use "flow create" instead.'));
103
10
  });
104
11
  }
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import confirm from '@inquirer/confirm';
2
3
  import chalk from 'chalk';
3
4
  import { createServices } from '../lib/services.js';
@@ -14,23 +15,18 @@ export async function runCheckout(branchName, useCases, services, deps) {
14
15
  }
15
16
  services.console.log('\nChecking for branch...');
16
17
  try {
17
- // Fetch all repos from source-path
18
+ // 1. Fetch all repos from source-path
18
19
  await useCases.fetchAllRepos.execute({
19
20
  sourcePath,
20
21
  fetchCacheTtlSeconds: config.fetchCacheTtlSeconds,
21
22
  });
22
- // Execute use case
23
- const result = await useCases.checkoutWorkspace.execute({
24
- branchName,
23
+ // 2. Discover repos and find which ones have the branch
24
+ const discoverResult = await useCases.discoverReposWithBranch.execute({
25
25
  sourcePath,
26
- destPath,
27
- copyFiles: config.copyFiles,
28
- tmux: config.tmux,
29
- postCheckout: shouldRunPostCheckout ? config.postCheckout : undefined,
30
- perRepoPostCheckout: shouldRunPostCheckout ? config.perRepoPostCheckout : {},
26
+ branchName,
31
27
  });
32
- // Display branch check results
33
- for (const checkResult of result.branchCheckResults) {
28
+ // 3. Display per-repo branch check results
29
+ for (const checkResult of discoverResult.branchCheckResults) {
34
30
  if (checkResult.error) {
35
31
  services.console.log(`${checkResult.repoName}... ${chalk.red(`error: ${checkResult.error}`)}`);
36
32
  }
@@ -41,12 +37,56 @@ export async function runCheckout(branchName, useCases, services, deps) {
41
37
  services.console.log(`${checkResult.repoName}... ${chalk.dim('no branch')}`);
42
38
  }
43
39
  }
44
- services.console.log(`\nCreated workspace at ${chalk.cyan(result.workspacePath)} with ${result.successCount}/${result.totalCount} repos.`);
45
- if (result.tmuxCreated) {
40
+ // 4. Throw error if no repos match
41
+ if (discoverResult.matchingRepos.length === 0) {
42
+ throw new Error(`Branch "${branchName}" not found in any repo.`);
43
+ }
44
+ // 5. Create workspace directory, placeholder config, AGENTS.md, tmux session
45
+ const workspaceResult = await useCases.createWorkspace.execute({
46
+ branchName,
47
+ sourcePath,
48
+ destPath,
49
+ tmux: config.tmux,
50
+ });
51
+ const { workspacePath, tmuxCreated } = workspaceResult;
52
+ const sessionName = tmuxCreated ? branchName : undefined;
53
+ // 6. For each matching repo in parallel: detect base branch, then addToWorkspace
54
+ const results = await Promise.allSettled(discoverResult.matchingRepos.map(async (repoPath) => {
55
+ const repoName = path.basename(repoPath);
56
+ // Resolve per-repo post-checkout command
57
+ const repoConf = services.repoConfig.load(repoPath);
58
+ const resolvedPostCheckout = shouldRunPostCheckout
59
+ ? services.repoConfig.resolvePostCheckout(repoName, config.perRepoPostCheckout, repoConf, config.postCheckout)
60
+ : undefined;
61
+ // Detect base branch for this repo
62
+ const baseBranch = await services.git.findFirstExistingBranch(repoPath, ['master', 'main', 'trunk', 'develop']) ?? 'master';
63
+ // Add repo to workspace (creates worktree, copies files, tmux pane, post-checkout)
64
+ return useCases.addToWorkspace.execute({
65
+ repoPath,
66
+ workspacePath,
67
+ branchName,
68
+ baseBranch,
69
+ sessionName,
70
+ copyFiles: config.copyFiles,
71
+ postCheckout: resolvedPostCheckout,
72
+ });
73
+ }));
74
+ // Tally results
75
+ const successCount = results.filter((r) => r.status === 'fulfilled').length;
76
+ const totalCount = results.length;
77
+ const postCheckoutResults = results
78
+ .filter((r) => r.status === 'fulfilled')
79
+ .map((r) => r.value)
80
+ .filter((r) => r.postCheckoutRan);
81
+ const postCheckoutSuccess = postCheckoutResults.filter((r) => r.postCheckoutSuccess).length;
82
+ const postCheckoutTotal = postCheckoutResults.length;
83
+ // 7. Display results
84
+ services.console.log(`\nCreated workspace at ${chalk.cyan(workspacePath)} with ${successCount}/${totalCount} repos.`);
85
+ if (tmuxCreated) {
46
86
  services.console.log(`Created tmux session: ${chalk.cyan(branchName)}`);
47
87
  }
48
- if (result.postCheckoutSuccess !== undefined) {
49
- services.console.log(`\nCompleted post-checkout in ${result.postCheckoutSuccess}/${result.postCheckoutTotal} workspace(s).`);
88
+ if (postCheckoutTotal > 0) {
89
+ services.console.log(`\nCompleted post-checkout in ${postCheckoutSuccess}/${postCheckoutTotal} workspace(s).`);
50
90
  }
51
91
  else if (!config.postCheckout) {
52
92
  services.console.log('\nTip: Configure a post-checkout command to run automatically after branching/checkout.');
@@ -0,0 +1,118 @@
1
+ import path from 'node:path';
2
+ import checkbox, { Separator } from '@inquirer/checkbox';
3
+ import input from '@inquirer/input';
4
+ import confirm from '@inquirer/confirm';
5
+ import chalk from 'chalk';
6
+ import { createServices } from '../lib/services.js';
7
+ import { createUseCases } from '../usecases/usecases.js';
8
+ import { NoReposFoundError } from '../lib/errors.js';
9
+ import { buildRepoCheckboxChoices } from './helpers.js';
10
+ export async function runCreate(branchName, useCases, services, deps) {
11
+ const { sourcePath, destPath } = services.config.getRequired();
12
+ const config = services.config.load();
13
+ const repos = services.repos.discoverRepos(sourcePath);
14
+ if (repos.length === 0) {
15
+ throw new NoReposFoundError(sourcePath);
16
+ }
17
+ // User prompts
18
+ const checkboxChoices = buildRepoCheckboxChoices(repos, services, config.branchAutoSelectRepos, (label) => new Separator(label));
19
+ const selected = await deps.checkbox({
20
+ message: `Select repos for branch "${branchName}":`,
21
+ choices: checkboxChoices,
22
+ pageSize: 20,
23
+ loop: false,
24
+ });
25
+ if (selected.length === 0) {
26
+ services.console.log('No repos selected.');
27
+ return;
28
+ }
29
+ const sourceBranch = await deps.input({
30
+ message: 'Branch from which branch?',
31
+ default: 'master',
32
+ });
33
+ let shouldRunPostCheckout = false;
34
+ if (config.postCheckout) {
35
+ shouldRunPostCheckout = await deps.confirm({
36
+ message: `Run "${config.postCheckout}" in all workspaces?`,
37
+ default: true,
38
+ });
39
+ }
40
+ services.console.log('\nCreating workspace...');
41
+ // Fetch all selected repos
42
+ await services.fetch.fetchRepos(selected, {
43
+ ttlSeconds: config.fetchCacheTtlSeconds,
44
+ });
45
+ // 1. Create workspace directory, placeholder config, AGENTS.md, tmux session
46
+ const workspaceResult = await useCases.createWorkspace.execute({
47
+ branchName,
48
+ sourcePath,
49
+ destPath,
50
+ tmux: config.tmux,
51
+ });
52
+ const { workspacePath, tmuxCreated } = workspaceResult;
53
+ const sessionName = tmuxCreated ? branchName : undefined;
54
+ // 2. For each selected repo in parallel: createBranch then addToWorkspace
55
+ const results = await Promise.allSettled(selected.map(async (repoPath) => {
56
+ const repoName = path.basename(repoPath);
57
+ // Resolve per-repo post-checkout command
58
+ const repoConf = services.repoConfig.load(repoPath);
59
+ const resolvedPostCheckout = shouldRunPostCheckout
60
+ ? services.repoConfig.resolvePostCheckout(repoName, config.perRepoPostCheckout, repoConf, config.postCheckout)
61
+ : undefined;
62
+ // a. Create branch in source repo
63
+ const branchResult = await useCases.createBranch.execute({
64
+ repoPath,
65
+ branchName,
66
+ sourceBranch,
67
+ });
68
+ // b. Add repo to workspace (creates worktree, copies files, tmux pane, post-checkout)
69
+ return useCases.addToWorkspace.execute({
70
+ repoPath,
71
+ workspacePath,
72
+ branchName,
73
+ baseBranch: branchResult.baseBranch,
74
+ sessionName,
75
+ copyFiles: config.copyFiles,
76
+ postCheckout: resolvedPostCheckout,
77
+ });
78
+ }));
79
+ // Track which repos were branched from
80
+ services.fetchCache.trackBranchUsage(selected.map((r) => path.basename(r)));
81
+ // Tally results
82
+ const successCount = results.filter((r) => r.status === 'fulfilled').length;
83
+ const totalCount = results.length;
84
+ const postCheckoutResults = results
85
+ .filter((r) => r.status === 'fulfilled')
86
+ .map((r) => r.value)
87
+ .filter((r) => r.postCheckoutRan);
88
+ const postCheckoutSuccess = postCheckoutResults.filter((r) => r.postCheckoutSuccess).length;
89
+ const postCheckoutTotal = postCheckoutResults.length;
90
+ // Display results
91
+ services.console.log(`\nCreated workspace at ${chalk.cyan(workspacePath)} with ${successCount}/${totalCount} repos.`);
92
+ if (tmuxCreated) {
93
+ services.console.log(`Created tmux session: ${chalk.cyan(branchName)}`);
94
+ }
95
+ if (postCheckoutTotal > 0) {
96
+ services.console.log(`\nCompleted post-checkout in ${postCheckoutSuccess}/${postCheckoutTotal} workspace(s).`);
97
+ }
98
+ else if (!config.postCheckout) {
99
+ services.console.log('\nTip: Configure a post-checkout command to run automatically after branching/checkout.');
100
+ services.console.log(' Example: flow config set post-checkout "npm ci"');
101
+ }
102
+ }
103
+ export function registerCreateCommand(program) {
104
+ program
105
+ .command('create <branch-name>')
106
+ .description('Create branches and worktrees for selected repos')
107
+ .action(async (branchName) => {
108
+ const services = createServices();
109
+ const useCases = createUseCases(services);
110
+ try {
111
+ await runCreate(branchName, useCases, services, { checkbox, input, confirm });
112
+ }
113
+ catch (error) {
114
+ services.console.error(error.message);
115
+ services.process.exit(1);
116
+ }
117
+ });
118
+ }
@@ -4,7 +4,7 @@ import { createServices } from '../lib/services.js';
4
4
  import { createUseCases } from '../usecases/usecases.js';
5
5
  import { StatusService } from '../lib/status.js';
6
6
  import { resolveWorkspace } from '../lib/workspaceResolver.js';
7
- export async function runRemove(branchName, useCases, services, deps) {
7
+ export async function runDrop(branchName, useCases, services, deps) {
8
8
  const { sourcePath } = services.config.getRequired();
9
9
  const config = services.config.load();
10
10
  const { workspacePath, displayName: branchNameForDisplay } = resolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
@@ -47,7 +47,7 @@ export async function runRemove(branchName, useCases, services, deps) {
47
47
  services.console.log(` Tmux session: ${chalk.cyan(branchNameForDisplay)}`);
48
48
  }
49
49
  const confirmed = await deps.confirm({
50
- message: 'Are you sure you want to remove this workspace?',
50
+ message: 'Are you sure you want to drop this workspace?',
51
51
  default: false,
52
52
  });
53
53
  if (!confirmed) {
@@ -55,7 +55,7 @@ export async function runRemove(branchName, useCases, services, deps) {
55
55
  services.process.exit(0);
56
56
  }
57
57
  // Execute use case (will throw if there are issues)
58
- services.console.log('\nRemoving workspace...');
58
+ services.console.log('\nDropping workspace...');
59
59
  const result = await useCases.removeWorkspace.execute({
60
60
  workspacePath,
61
61
  branchName: branchNameForDisplay,
@@ -85,17 +85,17 @@ export async function runRemove(branchName, useCases, services, deps) {
85
85
  if (result.tmuxKilled) {
86
86
  services.console.log(`${chalk.green('Killed tmux session:')} ${branchNameForDisplay}`);
87
87
  }
88
- services.console.log(`\n${chalk.green('Successfully removed workspace:')} ${branchNameForDisplay}`);
88
+ services.console.log(`\n${chalk.green('Successfully dropped workspace:')} ${branchNameForDisplay}`);
89
89
  }
90
- export function registerRemoveCommand(program) {
90
+ export function registerDropCommand(program) {
91
91
  program
92
- .command('remove [branch-name]')
93
- .description('Remove a workspace and all its worktrees (auto-detects from current directory if branch not provided)')
92
+ .command('drop [branch-name]')
93
+ .description('Drop a workspace and all its worktrees (auto-detects from current directory if branch not provided)')
94
94
  .action(async (branchName) => {
95
95
  const services = createServices();
96
96
  const useCases = createUseCases(services);
97
97
  try {
98
- await runRemove(branchName, useCases, services, { confirm });
98
+ await runDrop(branchName, useCases, services, { confirm });
99
99
  }
100
100
  catch (error) {
101
101
  services.console.error(error.message);
@@ -1,26 +1,39 @@
1
1
  import chalk from 'chalk';
2
2
  import { createServices } from '../lib/services.js';
3
3
  import { createUseCases } from '../usecases/usecases.js';
4
- export async function runFetch(useCases, services) {
4
+ import { tryResolveWorkspace } from '../lib/workspaceResolver.js';
5
+ export async function runFetch(branchName, useCases, services) {
5
6
  const { destPath, sourcePath } = services.config.getRequired();
6
- services.console.log('Fetching all repos used across workspaces...\n');
7
- await useCases.fetchUsedRepos.execute({
8
- destPath,
9
- sourcePath,
10
- fetchCacheTtlSeconds: 0, // Bypass cache
11
- silent: false,
12
- });
7
+ const workspace = tryResolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
8
+ if (workspace) {
9
+ services.console.log(`Fetching repos for workspace ${chalk.cyan(workspace.displayName)}...\n`);
10
+ await useCases.fetchWorkspaceRepos.execute({
11
+ workspacePath: workspace.workspacePath,
12
+ sourcePath,
13
+ fetchCacheTtlSeconds: 0,
14
+ silent: false,
15
+ });
16
+ }
17
+ else {
18
+ services.console.log('Fetching all repos used across workspaces...\n');
19
+ await useCases.fetchUsedRepos.execute({
20
+ destPath,
21
+ sourcePath,
22
+ fetchCacheTtlSeconds: 0,
23
+ silent: false,
24
+ });
25
+ }
13
26
  services.console.log(`\n${chalk.green('✓')} Fetch complete`);
14
27
  }
15
28
  export function registerFetchCommand(program) {
16
29
  program
17
- .command('fetch')
18
- .description('Fetch all repos used across workspaces (bypasses cache)')
19
- .action(async () => {
30
+ .command('fetch [branch-name]')
31
+ .description('Fetch repos (workspace-scoped if branch provided, all workspaces otherwise)')
32
+ .action(async (branchName) => {
20
33
  const services = createServices();
21
34
  const useCases = createUseCases(services);
22
35
  try {
23
- await runFetch(useCases, services);
36
+ await runFetch(branchName, useCases, services);
24
37
  }
25
38
  catch (error) {
26
39
  services.console.error(error.message);