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 +2 -0
- package/dist/lib/postCheckout.js +2 -21
- package/dist/lib/tmux.js +9 -0
- package/dist/usecases/checkoutWorkspace.js +10 -8
- package/dist/usecases/createBranchWorkspace.js +10 -8
- package/dist/usecases/resumeTmuxSessions.js +6 -19
- package/dist/usecases/runPostCheckout.js +49 -0
- package/dist/usecases/usecases.js +7 -3
- package/package.json +1 -1
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.
|
package/dist/lib/postCheckout.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
constructor(workspaceDir, workspaceConfig, worktree, repos, git, parallel, tmux,
|
|
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.
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
16
|
-
constructor(workspaceDir, workspaceConfig, worktree, repos, git, parallel, tmux,
|
|
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.
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
*
|
|
4
|
+
* Creates splits for all git directories (directories with .git).
|
|
6
5
|
*/
|
|
7
6
|
export class ResumeTmuxSessionsUseCase {
|
|
8
7
|
workspaceDir;
|
|
9
8
|
tmux;
|
|
10
|
-
|
|
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
|
-
//
|
|
28
|
+
// 2. Try to create tmux session for each workspace
|
|
38
29
|
for (const workspace of workspaces) {
|
|
39
30
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
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,
|
|
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,
|
|
23
|
-
checkoutWorkspace: new CheckoutWorkspaceUseCase(services.workspaceDir, services.workspaceConfig, services.worktree, services.repos, services.git, services.parallel, services.tmux,
|
|
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
|
|
33
|
+
resumeTmuxSessions: new ResumeTmuxSessionsUseCase(services.workspaceDir, services.tmux),
|
|
34
|
+
runPostCheckout,
|
|
31
35
|
};
|
|
32
36
|
}
|