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 +30 -6
- package/dist/cli.js +6 -2
- package/dist/commands/attach.js +120 -0
- package/dist/commands/branch.js +3 -96
- package/dist/commands/checkout.js +55 -15
- package/dist/commands/create.js +118 -0
- package/dist/commands/{remove.js → drop.js} +8 -8
- package/dist/commands/fetch.js +25 -12
- package/dist/commands/helpers.js +24 -0
- package/dist/commands/prune.js +0 -1
- package/dist/commands/quickstart.js +1 -1
- package/dist/commands/tmux.js +3 -3
- package/dist/lib/git.js +3 -0
- package/dist/lib/repoConfig.js +60 -0
- package/dist/lib/services.js +3 -0
- package/dist/lib/tmux.js +23 -0
- package/dist/lib/workspaceResolver.js +13 -0
- package/dist/usecases/addToWorkspace.js +75 -0
- package/dist/usecases/createBranch.js +37 -0
- package/dist/usecases/createWorkspace.js +37 -0
- package/dist/usecases/discoverReposWithBranch.js +25 -0
- package/dist/usecases/usecases.js +8 -8
- package/package.json +1 -1
- package/dist/usecases/checkoutWorkspace.js +0 -91
- package/dist/usecases/createBranchWorkspace.js +0 -86
- package/dist/usecases/runPostCheckout.js +0 -49
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
|
|
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
|
|
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
|
|
84
|
+
### `flow drop <name>`
|
|
81
85
|
|
|
82
|
-
|
|
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`
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/commands/branch.js
CHANGED
|
@@ -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('
|
|
93
|
-
.action(async (
|
|
6
|
+
.description(chalk.dim('Deprecated: use "flow create <branch-name>" instead'))
|
|
7
|
+
.action(async () => {
|
|
94
8
|
const services = createServices();
|
|
95
|
-
|
|
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
|
-
//
|
|
23
|
-
const
|
|
24
|
-
branchName,
|
|
23
|
+
// 2. Discover repos and find which ones have the branch
|
|
24
|
+
const discoverResult = await useCases.discoverReposWithBranch.execute({
|
|
25
25
|
sourcePath,
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
45
|
-
if (
|
|
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 (
|
|
49
|
-
services.console.log(`\nCompleted post-checkout in ${
|
|
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
|
|
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
|
|
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('\
|
|
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
|
|
88
|
+
services.console.log(`\n${chalk.green('Successfully dropped workspace:')} ${branchNameForDisplay}`);
|
|
89
89
|
}
|
|
90
|
-
export function
|
|
90
|
+
export function registerDropCommand(program) {
|
|
91
91
|
program
|
|
92
|
-
.command('
|
|
93
|
-
.description('
|
|
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
|
|
98
|
+
await runDrop(branchName, useCases, services, { confirm });
|
|
99
99
|
}
|
|
100
100
|
catch (error) {
|
|
101
101
|
services.console.error(error.message);
|
package/dist/commands/fetch.js
CHANGED
|
@@ -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
|
-
|
|
4
|
+
import { tryResolveWorkspace } from '../lib/workspaceResolver.js';
|
|
5
|
+
export async function runFetch(branchName, useCases, services) {
|
|
5
6
|
const { destPath, sourcePath } = services.config.getRequired();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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);
|