worktree-flow 0.0.17 → 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,9 +56,9 @@ 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 add [name]`
59
+ ### `flow attach [name]`
60
60
 
61
- Add 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.
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
62
 
63
63
  ### `flow pull`
64
64
 
@@ -81,15 +81,19 @@ List all workspaces with status indicators. Shows:
81
81
 
82
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.
83
83
 
84
- ### `flow remove <name>`
84
+ ### `flow drop <name>`
85
85
 
86
- 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.
87
87
 
88
- ### `flow prune` (alias: `clean`)
88
+ ### `flow prune`
89
89
 
90
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.
91
91
 
92
- ### `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`
93
97
 
94
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.
95
99
 
package/dist/cli.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { registerConfigCommand } from './commands/config.js';
4
- import { registerAddCommand } from './commands/add.js';
4
+ import { registerCreateCommand } from './commands/create.js';
5
+ import { registerAttachCommand } from './commands/attach.js';
5
6
  import { registerBranchCommand } from './commands/branch.js';
6
7
  import { registerCheckoutCommand } from './commands/checkout.js';
7
8
  import { registerListCommand } from './commands/list.js';
8
9
  import { registerPullCommand } from './commands/pull.js';
9
10
  import { registerPushCommand } from './commands/push.js';
10
- import { registerRemoveCommand } from './commands/remove.js';
11
+ import { registerDropCommand } from './commands/drop.js';
11
12
  import { registerStatusCommand } from './commands/status.js';
12
13
  import { registerPruneCommand } from './commands/prune.js';
13
14
  import { registerFetchCommand } from './commands/fetch.js';
@@ -19,13 +20,14 @@ program
19
20
  .description('Manage git worktrees across a poly-repo environment')
20
21
  .version('0.1.0');
21
22
  registerConfigCommand(program);
22
- registerAddCommand(program);
23
+ registerCreateCommand(program);
24
+ registerAttachCommand(program);
23
25
  registerBranchCommand(program);
24
26
  registerCheckoutCommand(program);
25
27
  registerListCommand(program);
26
28
  registerPullCommand(program);
27
29
  registerPushCommand(program);
28
- registerRemoveCommand(program);
30
+ registerDropCommand(program);
29
31
  registerStatusCommand(program);
30
32
  registerPruneCommand(program);
31
33
  registerFetchCommand(program);
@@ -8,7 +8,7 @@ import { createUseCases } from '../usecases/usecases.js';
8
8
  import { NoReposFoundError } from '../lib/errors.js';
9
9
  import { resolveWorkspace } from '../lib/workspaceResolver.js';
10
10
  import { buildRepoCheckboxChoices } from './helpers.js';
11
- export async function runAdd(branchName, useCases, services, deps) {
11
+ export async function runAttach(branchName, useCases, services, deps) {
12
12
  // 1. Resolve workspace (from arg or cwd)
13
13
  const { workspacePath, displayName } = resolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
14
14
  const { sourcePath } = services.config.getRequired();
@@ -29,9 +29,9 @@ export async function runAdd(branchName, useCases, services, deps) {
29
29
  return;
30
30
  }
31
31
  // 4. Repo picker (same pattern as branch command)
32
- const checkboxChoices = buildRepoCheckboxChoices(availableRepos, services, config, (label) => new Separator(label));
32
+ const checkboxChoices = buildRepoCheckboxChoices(availableRepos, services, [], (label) => new Separator(label));
33
33
  const selected = await deps.checkbox({
34
- message: `Select repos to add to "${displayName}":`,
34
+ message: `Select repos to attach to "${displayName}":`,
35
35
  choices: checkboxChoices,
36
36
  pageSize: 20,
37
37
  loop: false,
@@ -53,7 +53,7 @@ export async function runAdd(branchName, useCases, services, deps) {
53
53
  default: true,
54
54
  });
55
55
  }
56
- services.console.log('\nAdding repos to workspace...');
56
+ services.console.log('\nAttaching repos to workspace...');
57
57
  // 7. Fetch selected repos
58
58
  await services.fetch.fetchRepos(selected, {
59
59
  ttlSeconds: config.fetchCacheTtlSeconds,
@@ -97,20 +97,20 @@ export async function runAdd(branchName, useCases, services, deps) {
97
97
  const postCheckoutSuccess = postCheckoutResults.filter((r) => r.postCheckoutSuccess).length;
98
98
  const postCheckoutTotal = postCheckoutResults.length;
99
99
  // 10. Display results
100
- services.console.log(`\nAdded ${successCount}/${totalCount} repos to ${chalk.cyan(workspacePath)}.`);
100
+ services.console.log(`\nAttached ${successCount}/${totalCount} repos to ${chalk.cyan(workspacePath)}.`);
101
101
  if (postCheckoutTotal > 0) {
102
102
  services.console.log(`\nCompleted post-checkout in ${postCheckoutSuccess}/${postCheckoutTotal} workspace(s).`);
103
103
  }
104
104
  }
105
- export function registerAddCommand(program) {
105
+ export function registerAttachCommand(program) {
106
106
  program
107
- .command('add [branch-name]')
108
- .description('Add repos to an existing workspace (auto-detects from current directory if branch not provided)')
107
+ .command('attach [branch-name]')
108
+ .description('Attach repos to an existing workspace (auto-detects from current directory if branch not provided)')
109
109
  .action(async (branchName) => {
110
110
  const services = createServices();
111
111
  const useCases = createUseCases(services);
112
112
  try {
113
- await runAdd(branchName, useCases, services, { checkbox, input, confirm });
113
+ await runAttach(branchName, useCases, services, { checkbox, input, confirm });
114
114
  }
115
115
  catch (error) {
116
116
  services.console.error(error.message);
@@ -1,118 +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
- import { buildRepoCheckboxChoices } from './helpers.js';
10
- export async function runBranch(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, (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
3
  export function registerBranchCommand(program) {
104
4
  program
105
5
  .command('branch <branch-name>')
106
- .description('Create branches and worktrees for selected repos')
107
- .action(async (branchName) => {
6
+ .description(chalk.dim('Deprecated: use "flow create <branch-name>" instead'))
7
+ .action(async () => {
108
8
  const services = createServices();
109
- const useCases = createUseCases(services);
110
- try {
111
- await runBranch(branchName, useCases, services, { checkbox, input, confirm });
112
- }
113
- catch (error) {
114
- services.console.error(error.message);
115
- services.process.exit(1);
116
- }
9
+ services.console.log(chalk.yellow('⚠ "flow branch" is deprecated. Use "flow create" instead.'));
117
10
  });
118
11
  }
@@ -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);
@@ -53,10 +53,10 @@ export function logStatus(header, workspaces, linesToClear, getBaseBranch, conso
53
53
  *
54
54
  * @param createSeparator - factory from the display layer (e.g. inquirer's Separator constructor)
55
55
  */
56
- export function buildRepoCheckboxChoices(repos, services, config, createSeparator) {
56
+ export function buildRepoCheckboxChoices(repos, services, preSelected, createSeparator) {
57
57
  const choices = services.repos.formatRepoChoices(repos).map((choice) => ({
58
58
  ...choice,
59
- checked: config.branchAutoSelectRepos.includes(choice.name),
59
+ checked: preSelected.includes(choice.name),
60
60
  }));
61
61
  const recentlyUsed = new Set(services.fetchCache.getRecentlyUsedRepos(8));
62
62
  const commonlyUsed = choices.filter((c) => recentlyUsed.has(c.name));
@@ -89,7 +89,6 @@ export async function runPrune(useCases, services, deps) {
89
89
  export function registerPruneCommand(program) {
90
90
  program
91
91
  .command('prune')
92
- .alias('clean')
93
92
  .description('Select and remove workspaces')
94
93
  .action(async () => {
95
94
  const services = createServices();
@@ -133,7 +133,7 @@ export async function runQuickstart(services, deps) {
133
133
  services.console.log('');
134
134
  services.console.log(chalk.bold(' Get started:'));
135
135
  services.console.log('');
136
- services.console.log(` ${chalk.cyan('flow branch my-feature')} Create a new branch across repos`);
136
+ services.console.log(` ${chalk.cyan('flow create my-feature')} Create a new branch across repos`);
137
137
  services.console.log(` ${chalk.cyan('flow checkout my-feature')} Checkout an existing branch`);
138
138
  services.console.log(` ${chalk.cyan('flow list')} See all your workspaces`);
139
139
  services.console.log('');
@@ -1,7 +1,7 @@
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 runTmuxResume(useCases, services) {
4
+ export async function runTmuxSync(useCases, services) {
5
5
  const { destPath } = services.config.getRequired();
6
6
  const result = await useCases.resumeTmuxSessions.execute({ destPath });
7
7
  if (result.totalWorkspaces === 0) {
@@ -29,13 +29,13 @@ export function registerTmuxCommand(program) {
29
29
  .command('tmux')
30
30
  .description('Manage tmux sessions for workspaces');
31
31
  tmuxCommand
32
- .command('resume')
32
+ .command('sync')
33
33
  .description('Create tmux sessions for all workspaces that don\'t have one')
34
34
  .action(async () => {
35
35
  const services = createServices();
36
36
  const useCases = createUseCases(services);
37
37
  try {
38
- await runTmuxResume(useCases, services);
38
+ await runTmuxSync(useCases, services);
39
39
  }
40
40
  catch (error) {
41
41
  services.console.error(error.message);
@@ -39,3 +39,16 @@ export function resolveWorkspace(branchName, workspaceDir, config, process) {
39
39
  };
40
40
  }
41
41
  }
42
+ /**
43
+ * Like resolveWorkspace, but returns null instead of throwing when no workspace
44
+ * can be resolved. Useful when workspace resolution is optional (e.g. falling back
45
+ * to a broader operation when not in a workspace).
46
+ */
47
+ export function tryResolveWorkspace(branchName, workspaceDir, config, process) {
48
+ try {
49
+ return resolveWorkspace(branchName, workspaceDir, config, process);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
@@ -22,9 +22,13 @@ export class CreateBranchUseCase {
22
22
  }
23
23
  actualBaseBranch = fallback;
24
24
  }
25
- // 3. Create the branch from origin/<actualBaseBranch>
26
- await this.git.createBranch(params.repoPath, params.branchName, `origin/${actualBaseBranch}`);
27
- // 4. Return the actual base branch used
25
+ // 3. If the target branch already exists, skip creation and use it as-is
26
+ const targetBranchExists = await this.git.localRemoteBranchExists(params.repoPath, params.branchName);
27
+ if (!targetBranchExists) {
28
+ // 4. Create the branch from origin/<actualBaseBranch>
29
+ await this.git.createBranch(params.repoPath, params.branchName, `origin/${actualBaseBranch}`);
30
+ }
31
+ // 5. Return the actual base branch used
28
32
  return {
29
33
  repoName,
30
34
  baseBranch: actualBaseBranch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-flow",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Manage git worktrees across a poly-repo environment",
5
5
  "type": "module",
6
6
  "bin": {