worktree-flow 0.0.12 → 0.0.13

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
@@ -126,6 +126,8 @@ Configure different commands for specific repos by editing `~/.config/flow/confi
126
126
 
127
127
  Repos with per-repo commands use those; others fall back to the global `post-checkout` command.
128
128
 
129
+ 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.
130
+
129
131
  ## AGENTS.md
130
132
 
131
133
  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.
@@ -1,4 +1,3 @@
1
- import path from 'node:path';
2
1
  /**
3
2
  * PostCheckoutService handles execution of post-checkout commands.
4
3
  */
@@ -7,25 +6,7 @@ export class PostCheckoutService {
7
6
  constructor(shell) {
8
7
  this.shell = shell;
9
8
  }
10
- async runCommand(worktreeDirs, globalCommand, perRepoCommands) {
11
- // Determine command for each worktree (per-repo overrides global)
12
- const commandsToRun = worktreeDirs
13
- .map((dir) => {
14
- const repoName = path.basename(dir);
15
- const command = perRepoCommands[repoName] ?? globalCommand;
16
- return command ? { dir, command } : null;
17
- })
18
- .filter((x) => x !== null);
19
- let successCount = 0;
20
- await Promise.allSettled(commandsToRun.map(async ({ dir, command }) => {
21
- try {
22
- await this.shell.execFile('sh', ['-c', command], { cwd: dir });
23
- successCount++;
24
- }
25
- catch {
26
- // Error handled by caller
27
- }
28
- }));
29
- return { successCount, totalCount: commandsToRun.length };
9
+ async runCommandInDirectory(directory, command) {
10
+ await this.shell.execFile('sh', ['-c', command], { cwd: directory });
30
11
  }
31
12
  }
package/dist/lib/tmux.js CHANGED
@@ -44,6 +44,15 @@ export class TmuxService {
44
44
  }
45
45
  }
46
46
  }
47
+ async sendKeysToPane(sessionName, paneIndex, command) {
48
+ await this.shell.execFile('tmux', [
49
+ 'send-keys',
50
+ '-t',
51
+ `${sessionName}:0.${paneIndex}`,
52
+ command,
53
+ 'Enter',
54
+ ]);
55
+ }
47
56
  async killSession(sessionName) {
48
57
  try {
49
58
  await this.shell.execFile('tmux', ['kill-session', '-t', sessionName]);
@@ -13,8 +13,8 @@ export class CheckoutWorkspaceUseCase {
13
13
  git;
14
14
  parallel;
15
15
  tmux;
16
- postCheckout;
17
- constructor(workspaceDir, workspaceConfig, worktree, repos, git, parallel, tmux, postCheckout) {
16
+ runPostCheckout;
17
+ constructor(workspaceDir, workspaceConfig, worktree, repos, git, parallel, tmux, runPostCheckout) {
18
18
  this.workspaceDir = workspaceDir;
19
19
  this.workspaceConfig = workspaceConfig;
20
20
  this.worktree = worktree;
@@ -22,7 +22,7 @@ export class CheckoutWorkspaceUseCase {
22
22
  this.git = git;
23
23
  this.parallel = parallel;
24
24
  this.tmux = tmux;
25
- this.postCheckout = postCheckout;
25
+ this.runPostCheckout = runPostCheckout;
26
26
  }
27
27
  async execute(params) {
28
28
  // 1. Discover all repos
@@ -69,11 +69,13 @@ export class CheckoutWorkspaceUseCase {
69
69
  }
70
70
  }
71
71
  // 9. Run post-checkout if configured
72
- let postCheckoutResult;
73
- if (params.postCheckout || params.perRepoPostCheckout) {
74
- const worktreeDirs = this.workspaceDir.getWorktreeDirs(workspacePath);
75
- postCheckoutResult = await this.postCheckout.runCommand(worktreeDirs, params.postCheckout, params.perRepoPostCheckout ?? {});
76
- }
72
+ const postCheckoutResult = await this.runPostCheckout.execute({
73
+ workspacePath,
74
+ sessionName: tmuxCreated ? params.branchName : undefined,
75
+ tmuxEnabled: tmuxCreated,
76
+ postCheckout: params.postCheckout,
77
+ perRepoPostCheckout: params.perRepoPostCheckout,
78
+ });
77
79
  return {
78
80
  workspacePath,
79
81
  matchingRepos: matching.length,
@@ -12,8 +12,8 @@ export class CreateBranchWorkspaceUseCase {
12
12
  git;
13
13
  parallel;
14
14
  tmux;
15
- postCheckout;
16
- constructor(workspaceDir, workspaceConfig, worktree, repos, git, parallel, tmux, postCheckout) {
15
+ runPostCheckout;
16
+ constructor(workspaceDir, workspaceConfig, worktree, repos, git, parallel, tmux, runPostCheckout) {
17
17
  this.workspaceDir = workspaceDir;
18
18
  this.workspaceConfig = workspaceConfig;
19
19
  this.worktree = worktree;
@@ -21,7 +21,7 @@ export class CreateBranchWorkspaceUseCase {
21
21
  this.git = git;
22
22
  this.parallel = parallel;
23
23
  this.tmux = tmux;
24
- this.postCheckout = postCheckout;
24
+ this.runPostCheckout = runPostCheckout;
25
25
  }
26
26
  async execute(params) {
27
27
  // 1. Create workspace directory
@@ -66,11 +66,13 @@ export class CreateBranchWorkspaceUseCase {
66
66
  }
67
67
  }
68
68
  // 6. Run post-checkout command if configured
69
- let postCheckoutResult;
70
- if (params.postCheckout || params.perRepoPostCheckout) {
71
- const worktreeDirs = this.workspaceDir.getWorktreeDirs(workspacePath);
72
- postCheckoutResult = await this.postCheckout.runCommand(worktreeDirs, params.postCheckout, params.perRepoPostCheckout ?? {});
73
- }
69
+ const postCheckoutResult = await this.runPostCheckout.execute({
70
+ workspacePath,
71
+ sessionName: tmuxCreated ? params.branchName : undefined,
72
+ tmuxEnabled: tmuxCreated,
73
+ postCheckout: params.postCheckout,
74
+ perRepoPostCheckout: params.perRepoPostCheckout,
75
+ });
74
76
  return {
75
77
  workspacePath,
76
78
  successCount,
@@ -1,19 +1,14 @@
1
- import path from 'node:path';
2
1
  /**
3
2
  * Use case for resuming tmux sessions across all workspaces.
4
3
  * Creates sessions for workspaces that don't already have one.
5
- * Only creates splits for directories that match repos from source-path.
4
+ * Creates splits for all git directories (directories with .git).
6
5
  */
7
6
  export class ResumeTmuxSessionsUseCase {
8
7
  workspaceDir;
9
8
  tmux;
10
- repos;
11
- config;
12
- constructor(workspaceDir, tmux, repos, config) {
9
+ constructor(workspaceDir, tmux) {
13
10
  this.workspaceDir = workspaceDir;
14
11
  this.tmux = tmux;
15
- this.repos = repos;
16
- this.config = config;
17
12
  }
18
13
  async execute(params) {
19
14
  // 1. List all workspaces
@@ -27,24 +22,16 @@ export class ResumeTmuxSessionsUseCase {
27
22
  errors: [],
28
23
  };
29
24
  }
30
- // 2. Get valid repo names from source-path
31
- const { sourcePath } = this.config.getRequired();
32
- const repoPaths = this.repos.discoverRepos(sourcePath);
33
- const validRepoNames = new Set(repoPaths.map(repoPath => path.basename(repoPath)));
34
25
  let sessionsCreated = 0;
35
26
  let sessionsSkipped = 0;
36
27
  const errors = [];
37
- // 3. Try to create tmux session for each workspace
28
+ // 2. Try to create tmux session for each workspace
38
29
  for (const workspace of workspaces) {
39
30
  try {
40
- const allWorktreeDirs = this.workspaceDir.getWorktreeDirs(workspace.path);
41
- // Filter to only include directories that match repo names from source-path
42
- const filteredWorktreeDirs = allWorktreeDirs.filter(worktreeDir => {
43
- const dirName = path.basename(worktreeDir);
44
- return validRepoNames.has(dirName);
45
- });
31
+ // Get all git directories (getWorktreeDirs now filters to only include dirs with .git)
32
+ const worktreeDirs = this.workspaceDir.getWorktreeDirs(workspace.path);
46
33
  // Try to create session - will throw on duplicate session
47
- await this.tmux.createSession(workspace.path, workspace.name, filteredWorktreeDirs);
34
+ await this.tmux.createSession(workspace.path, workspace.name, worktreeDirs);
48
35
  sessionsCreated++;
49
36
  }
50
37
  catch (error) {
@@ -0,0 +1,49 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Use case for running post-checkout commands.
4
+ * Runs commands either directly in worktree directories or in tmux panes.
5
+ */
6
+ export class RunPostCheckoutUseCase {
7
+ workspaceDir;
8
+ postCheckout;
9
+ tmux;
10
+ constructor(workspaceDir, postCheckout, tmux) {
11
+ this.workspaceDir = workspaceDir;
12
+ this.postCheckout = postCheckout;
13
+ this.tmux = tmux;
14
+ }
15
+ async execute(params) {
16
+ // Skip if no commands configured
17
+ if (!params.postCheckout && (!params.perRepoPostCheckout || Object.keys(params.perRepoPostCheckout).length === 0)) {
18
+ return undefined;
19
+ }
20
+ // Get worktree directories
21
+ const worktreeDirs = this.workspaceDir.getWorktreeDirs(params.workspacePath);
22
+ // Map each worktree to its command (per-repo overrides global)
23
+ const commandsToRun = worktreeDirs
24
+ .map((dir, index) => {
25
+ const repoName = path.basename(dir);
26
+ const command = params.perRepoPostCheckout?.[repoName] ?? params.postCheckout;
27
+ return command ? { dir, command, paneIndex: index + 1 } : null;
28
+ })
29
+ .filter((x) => x !== null);
30
+ // Run commands in parallel
31
+ let successCount = 0;
32
+ await Promise.allSettled(commandsToRun.map(async ({ dir, command, paneIndex }) => {
33
+ try {
34
+ if (params.tmuxEnabled && params.sessionName) {
35
+ // Panes are indexed from 0, first pane (root) is 0, worktrees start at 1
36
+ await this.tmux.sendKeysToPane(params.sessionName, paneIndex, command);
37
+ }
38
+ else {
39
+ await this.postCheckout.runCommandInDirectory(dir, command);
40
+ }
41
+ successCount++;
42
+ }
43
+ catch {
44
+ // Error handled by counting successes
45
+ }
46
+ }));
47
+ return { successCount, totalCount: commandsToRun.length };
48
+ }
49
+ }
@@ -10,23 +10,27 @@ import { FetchAllReposUseCase } from './fetchAllRepos.js';
10
10
  import { FetchWorkspaceReposUseCase } from './fetchWorkspaceRepos.js';
11
11
  import { FetchUsedReposUseCase } from './fetchUsedRepos.js';
12
12
  import { ResumeTmuxSessionsUseCase } from './resumeTmuxSessions.js';
13
+ import { RunPostCheckoutUseCase } from './runPostCheckout.js';
13
14
  /**
14
15
  * Factory function for creating all use cases with their service dependencies.
15
16
  * Use cases orchestrate workflows by coordinating multiple services.
16
17
  */
17
18
  export function createUseCases(services) {
19
+ // Create use cases that are dependencies first
20
+ const runPostCheckout = new RunPostCheckoutUseCase(services.workspaceDir, services.postCheckout, services.tmux);
18
21
  return {
19
22
  fetchAllRepos: new FetchAllReposUseCase(services.fetch, services.repos),
20
23
  fetchWorkspaceRepos: new FetchWorkspaceReposUseCase(services.workspaceDir, services.fetch),
21
24
  fetchUsedRepos: new FetchUsedReposUseCase(services.workspaceDir, services.fetch),
22
- createBranchWorkspace: new CreateBranchWorkspaceUseCase(services.workspaceDir, services.workspaceConfig, services.worktree, services.repos, services.git, services.parallel, services.tmux, services.postCheckout),
23
- checkoutWorkspace: new CheckoutWorkspaceUseCase(services.workspaceDir, services.workspaceConfig, services.worktree, services.repos, services.git, services.parallel, services.tmux, services.postCheckout),
25
+ createBranchWorkspace: new CreateBranchWorkspaceUseCase(services.workspaceDir, services.workspaceConfig, services.worktree, services.repos, services.git, services.parallel, services.tmux, runPostCheckout),
26
+ checkoutWorkspace: new CheckoutWorkspaceUseCase(services.workspaceDir, services.workspaceConfig, services.worktree, services.repos, services.git, services.parallel, services.tmux, runPostCheckout),
24
27
  removeWorkspace: new RemoveWorkspaceUseCase(services.workspaceDir, services.workspaceConfig, services.worktree, services.repos, services.status, services.tmux),
25
28
  pushWorkspace: new PushWorkspaceUseCase(services.workspaceDir, services.git, services.parallel),
26
29
  pullWorkspace: new PullWorkspaceUseCase(services.workspaceDir, services.git, services.parallel),
27
30
  checkWorkspaceStatus: new CheckWorkspaceStatusUseCase(services.workspaceDir, services.workspaceConfig, services.status),
28
31
  discoverPrunableWorkspaces: new DiscoverPrunableWorkspacesUseCase(services.workspaceDir, services.workspaceConfig, services.status, services.git),
29
32
  listWorkspacesWithStatus: new ListWorkspacesWithStatusUseCase(services.workspaceDir, services.workspaceConfig, services.status),
30
- resumeTmuxSessions: new ResumeTmuxSessionsUseCase(services.workspaceDir, services.tmux, services.repos, services.config),
33
+ resumeTmuxSessions: new ResumeTmuxSessionsUseCase(services.workspaceDir, services.tmux),
34
+ runPostCheckout,
31
35
  };
32
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-flow",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Manage git worktrees across a poly-repo environment",
5
5
  "type": "module",
6
6
  "bin": {