worktree-flow 0.0.3 → 0.0.4
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 +8 -3
- package/dist/commands/list.js +69 -8
- package/dist/commands/prune.js +1 -5
- package/dist/lib/fetch.js +13 -8
- package/dist/lib/workspaceReposCollector.js +26 -0
- package/dist/usecases/discoverPrunableWorkspaces.js +4 -17
- package/dist/usecases/listWorkspacesWithStatus.js +48 -0
- package/dist/usecases/usecases.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,15 +68,20 @@ Push all repos in the current workspace. Automatically sets upstream on first pu
|
|
|
68
68
|
|
|
69
69
|
Check the status of all repos in a workspace. Shows uncommitted changes, commits ahead of main, and up-to-date repos.
|
|
70
70
|
|
|
71
|
-
### `flow list`
|
|
71
|
+
### `flow list` (alias: `ls`)
|
|
72
72
|
|
|
73
|
-
List all workspaces
|
|
73
|
+
List all workspaces with status indicators. Shows:
|
|
74
|
+
- Active workspace (marked with `*`) based on current directory
|
|
75
|
+
- Overall status: `clean`, `uncommitted`, `ahead`, `behind`, `diverged`, or `mixed`
|
|
76
|
+
- Repo count for each workspace
|
|
77
|
+
|
|
78
|
+
Fetches all repos before checking status to ensure accurate information.
|
|
74
79
|
|
|
75
80
|
### `flow remove <name>`
|
|
76
81
|
|
|
77
82
|
Remove a workspace and all its worktrees. Fetches latest, checks for uncommitted changes and unpushed commits, and prompts for confirmation before removing.
|
|
78
83
|
|
|
79
|
-
### `flow prune`
|
|
84
|
+
### `flow prune` (alias: `clean`)
|
|
80
85
|
|
|
81
86
|
Remove stale workspaces in bulk. Finds workspaces where all worktrees are clean, fully merged, and haven't been committed to in over 7 days.
|
|
82
87
|
|
package/dist/commands/list.js
CHANGED
|
@@ -1,26 +1,87 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { createServices } from '../lib/services.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
3
|
+
import { createUseCases } from '../usecases/usecases.js';
|
|
4
|
+
function getStatusIndicator(workspace) {
|
|
5
|
+
const hasUncommitted = workspace.statuses.some(s => s.status.type === 'uncommitted');
|
|
6
|
+
const hasAhead = workspace.statuses.some(s => s.status.type === 'ahead');
|
|
7
|
+
const hasBehind = workspace.statuses.some(s => s.status.type === 'behind');
|
|
8
|
+
const hasDiverged = workspace.statuses.some(s => s.status.type === 'diverged');
|
|
9
|
+
const hasError = workspace.statuses.some(s => s.status.type === 'error');
|
|
10
|
+
if (hasUncommitted) {
|
|
11
|
+
return chalk.yellow('uncommitted');
|
|
12
|
+
}
|
|
13
|
+
else if (hasDiverged) {
|
|
14
|
+
return chalk.red('diverged');
|
|
15
|
+
}
|
|
16
|
+
else if (hasAhead && hasBehind) {
|
|
17
|
+
return chalk.yellow('ahead');
|
|
18
|
+
}
|
|
19
|
+
else if (hasAhead) {
|
|
20
|
+
return chalk.yellow('ahead');
|
|
21
|
+
}
|
|
22
|
+
else if (hasBehind) {
|
|
23
|
+
return chalk.blue('behind');
|
|
24
|
+
}
|
|
25
|
+
else if (hasError) {
|
|
26
|
+
return chalk.red('error');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return chalk.green('clean');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function runList(useCases, services) {
|
|
33
|
+
const { destPath, sourcePath } = services.config.getRequired();
|
|
34
|
+
const config = services.config.load();
|
|
35
|
+
const cwd = services.process.cwd();
|
|
36
|
+
// Get basic workspace list immediately
|
|
37
|
+
const basicWorkspaces = services.workspaceDir.listWorkspaces(destPath);
|
|
38
|
+
if (basicWorkspaces.length === 0) {
|
|
7
39
|
services.console.log('No workspaces found.');
|
|
8
40
|
return;
|
|
9
41
|
}
|
|
42
|
+
// Phase 1: Show basic list immediately
|
|
10
43
|
services.console.log(chalk.bold('\nWorkspaces:'));
|
|
11
|
-
for (const workspace of
|
|
12
|
-
|
|
44
|
+
for (const workspace of basicWorkspaces) {
|
|
45
|
+
const repoCount = chalk.dim(`(${workspace.repoCount} repo${workspace.repoCount === 1 ? '' : 's'})`);
|
|
46
|
+
services.console.log(` ${chalk.cyan(workspace.name)} ${repoCount} ${chalk.dim('fetching...')}`);
|
|
13
47
|
}
|
|
14
48
|
services.console.log('');
|
|
49
|
+
// Phase 2: Fetch and check status
|
|
50
|
+
const result = await useCases.listWorkspacesWithStatus.execute({
|
|
51
|
+
destPath,
|
|
52
|
+
sourcePath,
|
|
53
|
+
mainBranch: config.mainBranch,
|
|
54
|
+
cwd,
|
|
55
|
+
});
|
|
56
|
+
// Phase 3: Clear previous output and re-print with status
|
|
57
|
+
// Lines to clear:
|
|
58
|
+
// - 2 lines from '\nWorkspaces:' (blank line + header)
|
|
59
|
+
// - N workspace lines
|
|
60
|
+
// - 1 empty line after workspaces
|
|
61
|
+
const linesToClear = 2 + basicWorkspaces.length + 1;
|
|
62
|
+
for (let i = 0; i < linesToClear; i++) {
|
|
63
|
+
services.console.write('\x1b[1A'); // Move cursor up one line
|
|
64
|
+
services.console.write('\x1b[2K'); // Clear entire line
|
|
65
|
+
}
|
|
66
|
+
// Re-print with full status information
|
|
67
|
+
services.console.log(chalk.bold('\nWorkspaces:'));
|
|
68
|
+
for (const workspace of result.workspaces) {
|
|
69
|
+
const statusIndicator = getStatusIndicator(workspace);
|
|
70
|
+
const activeIndicator = workspace.isActive ? chalk.green('* ') : ' ';
|
|
71
|
+
const repoCount = chalk.dim(`(${workspace.repoCount} repo${workspace.repoCount === 1 ? '' : 's'})`);
|
|
72
|
+
services.console.log(`${activeIndicator}${chalk.cyan(workspace.name)} ${repoCount} ${statusIndicator}`);
|
|
73
|
+
}
|
|
15
74
|
}
|
|
16
75
|
export function registerListCommand(program) {
|
|
17
76
|
program
|
|
18
77
|
.command('list')
|
|
19
|
-
.
|
|
78
|
+
.alias('ls')
|
|
79
|
+
.description('List all workspaces with status indicators')
|
|
20
80
|
.action(async () => {
|
|
21
81
|
const services = createServices();
|
|
82
|
+
const useCases = createUseCases(services);
|
|
22
83
|
try {
|
|
23
|
-
await runList(services);
|
|
84
|
+
await runList(useCases, services);
|
|
24
85
|
}
|
|
25
86
|
catch (error) {
|
|
26
87
|
services.console.error(error.message);
|
package/dist/commands/prune.js
CHANGED
|
@@ -31,11 +31,6 @@ export async function runPrune(useCases, services, deps) {
|
|
|
31
31
|
services.console.log(` Status: ${chalk.green('clean')}`);
|
|
32
32
|
services.console.log('');
|
|
33
33
|
}
|
|
34
|
-
// Summary
|
|
35
|
-
const totalRepos = result.prunable.reduce((sum, ws) => sum + ws.repoCount, 0);
|
|
36
|
-
services.console.log(`${chalk.yellow('This will remove:')}`);
|
|
37
|
-
services.console.log(` ${result.prunable.length} workspace(s)`);
|
|
38
|
-
services.console.log(` ${totalRepos} worktree(s)`);
|
|
39
34
|
const confirmed = await deps.confirm({
|
|
40
35
|
message: 'Are you sure you want to prune these workspaces?',
|
|
41
36
|
default: false,
|
|
@@ -78,6 +73,7 @@ export async function runPrune(useCases, services, deps) {
|
|
|
78
73
|
export function registerPruneCommand(program) {
|
|
79
74
|
program
|
|
80
75
|
.command('prune')
|
|
76
|
+
.alias('clean')
|
|
81
77
|
.description('Remove old workspaces with no uncommitted changes and commits older than 7 days')
|
|
82
78
|
.action(async () => {
|
|
83
79
|
const services = createServices();
|
package/dist/lib/fetch.js
CHANGED
|
@@ -10,14 +10,17 @@ export class FetchService {
|
|
|
10
10
|
this.git = git;
|
|
11
11
|
this.console = console;
|
|
12
12
|
}
|
|
13
|
-
async fetchRepos(repoPaths) {
|
|
13
|
+
async fetchRepos(repoPaths, options) {
|
|
14
14
|
if (repoPaths.length === 0) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
+
const silent = options?.silent ?? false;
|
|
17
18
|
const total = repoPaths.length;
|
|
18
19
|
let completed = 0;
|
|
19
20
|
let failed = 0;
|
|
20
21
|
const updateProgress = () => {
|
|
22
|
+
if (silent)
|
|
23
|
+
return;
|
|
21
24
|
const percent = Math.floor((completed / total) * 100);
|
|
22
25
|
const message = `Fetching repos... ${completed}/${total} (${percent}%)`;
|
|
23
26
|
this.console.write(`\r${message}`);
|
|
@@ -46,13 +49,15 @@ export class FetchService {
|
|
|
46
49
|
// Start workers (up to concurrency limit)
|
|
47
50
|
const workers = Array.from({ length: Math.min(FETCH_CONCURRENCY, repoPaths.length) }, () => worker());
|
|
48
51
|
await Promise.all(workers);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
if (!silent) {
|
|
53
|
+
// Clear the progress line and show summary
|
|
54
|
+
this.console.write('\r');
|
|
55
|
+
if (failed > 0) {
|
|
56
|
+
this.console.log(`Fetched ${total - failed}/${total} repos ${chalk.yellow(`(${failed} failed)`)}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.console.log(`Fetched ${total} repos ${chalk.green('✓')}`);
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Collects all worktree directories and unique source repos across workspaces.
|
|
4
|
+
* This helper extracts common logic used by multiple use cases that need to
|
|
5
|
+
* fetch repos before analyzing workspace status.
|
|
6
|
+
*/
|
|
7
|
+
export function collectWorkspaceRepos(workspaces, workspaceDir, sourcePath) {
|
|
8
|
+
const uniqueSourceRepos = new Set();
|
|
9
|
+
const workspaceWorktrees = new Map();
|
|
10
|
+
for (const workspace of workspaces) {
|
|
11
|
+
const worktreeDirs = workspaceDir.getWorktreeDirs(workspace.path);
|
|
12
|
+
if (worktreeDirs.length > 0) {
|
|
13
|
+
workspaceWorktrees.set(workspace.path, worktreeDirs);
|
|
14
|
+
// Extract repo names and build source repo paths
|
|
15
|
+
worktreeDirs.forEach((worktreePath) => {
|
|
16
|
+
const repoName = path.basename(worktreePath);
|
|
17
|
+
const sourceRepoPath = path.join(sourcePath, repoName);
|
|
18
|
+
uniqueSourceRepos.add(sourceRepoPath);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
uniqueSourceRepos: Array.from(uniqueSourceRepos),
|
|
24
|
+
workspaceWorktrees,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { StatusService as StatusServiceClass } from '../lib/status.js';
|
|
2
|
+
import { collectWorkspaceRepos } from '../lib/workspaceReposCollector.js';
|
|
3
3
|
/**
|
|
4
4
|
* Use case for analyzing workspaces to find candidates for pruning.
|
|
5
5
|
* Returns workspaces where all worktrees have no issues and commits are older than specified days.
|
|
@@ -20,25 +20,12 @@ export class DiscoverPrunableWorkspacesUseCase {
|
|
|
20
20
|
const prunable = [];
|
|
21
21
|
const cutoffDate = new Date(Date.now() - params.daysOld * 24 * 60 * 60 * 1000);
|
|
22
22
|
// Collect all worktree directories and unique source repos across all workspaces
|
|
23
|
-
const uniqueSourceRepos =
|
|
24
|
-
const workspaceWorktrees = new Map();
|
|
25
|
-
for (const workspace of workspaces) {
|
|
26
|
-
const worktreeDirs = this.workspaceDir.getWorktreeDirs(workspace.path);
|
|
27
|
-
if (worktreeDirs.length > 0) {
|
|
28
|
-
workspaceWorktrees.set(workspace.path, worktreeDirs);
|
|
29
|
-
// Extract repo names and build source repo paths
|
|
30
|
-
worktreeDirs.forEach((worktreePath) => {
|
|
31
|
-
const repoName = path.basename(worktreePath);
|
|
32
|
-
const sourceRepoPath = path.join(params.sourcePath, repoName);
|
|
33
|
-
uniqueSourceRepos.add(sourceRepoPath);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
23
|
+
const { uniqueSourceRepos, workspaceWorktrees } = collectWorkspaceRepos(workspaces, this.workspaceDir, params.sourcePath);
|
|
37
24
|
// Fetch all unique source repos once
|
|
38
25
|
// Since worktrees share the same git repository, fetching in one worktree
|
|
39
26
|
// updates the remote refs for all worktrees of that repository
|
|
40
|
-
if (uniqueSourceRepos.
|
|
41
|
-
await this.fetch.fetchRepos(
|
|
27
|
+
if (uniqueSourceRepos.length > 0) {
|
|
28
|
+
await this.fetch.fetchRepos(uniqueSourceRepos);
|
|
42
29
|
}
|
|
43
30
|
// Now analyze each workspace
|
|
44
31
|
for (const workspace of workspaces) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { collectWorkspaceRepos } from '../lib/workspaceReposCollector.js';
|
|
2
|
+
/**
|
|
3
|
+
* Use case for listing all workspaces with their status information.
|
|
4
|
+
* Fetches all repos and checks status for each workspace's worktrees.
|
|
5
|
+
*/
|
|
6
|
+
export class ListWorkspacesWithStatusUseCase {
|
|
7
|
+
workspaceDir;
|
|
8
|
+
fetch;
|
|
9
|
+
status;
|
|
10
|
+
constructor(workspaceDir, fetch, status) {
|
|
11
|
+
this.workspaceDir = workspaceDir;
|
|
12
|
+
this.fetch = fetch;
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
async execute(params) {
|
|
16
|
+
const workspaces = this.workspaceDir.listWorkspaces(params.destPath);
|
|
17
|
+
if (workspaces.length === 0) {
|
|
18
|
+
return { workspaces: [] };
|
|
19
|
+
}
|
|
20
|
+
// Collect all worktree directories and unique source repos across all workspaces
|
|
21
|
+
const { uniqueSourceRepos, workspaceWorktrees } = collectWorkspaceRepos(workspaces, this.workspaceDir, params.sourcePath);
|
|
22
|
+
// Fetch all unique source repos once (silently to avoid UI jumping)
|
|
23
|
+
if (uniqueSourceRepos.length > 0) {
|
|
24
|
+
await this.fetch.fetchRepos(uniqueSourceRepos, { silent: true });
|
|
25
|
+
}
|
|
26
|
+
// Check status and detect active workspace
|
|
27
|
+
const workspacesWithStatus = [];
|
|
28
|
+
for (const workspace of workspaces) {
|
|
29
|
+
const worktreeDirs = workspaceWorktrees.get(workspace.path);
|
|
30
|
+
// Skip workspaces with no worktrees
|
|
31
|
+
if (!worktreeDirs || worktreeDirs.length === 0) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Check if this workspace is active (contains current working directory)
|
|
35
|
+
const isActive = this.workspaceDir.detectWorkspace(params.cwd, params.destPath) === workspace.path;
|
|
36
|
+
// Check status for all worktrees
|
|
37
|
+
const statuses = await this.status.checkAllWorktrees(worktreeDirs, params.mainBranch);
|
|
38
|
+
workspacesWithStatus.push({
|
|
39
|
+
name: workspace.name,
|
|
40
|
+
path: workspace.path,
|
|
41
|
+
repoCount: workspace.repoCount,
|
|
42
|
+
isActive,
|
|
43
|
+
statuses,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return { workspaces: workspacesWithStatus };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -5,6 +5,7 @@ import { PushWorkspaceUseCase } from './pushWorkspace.js';
|
|
|
5
5
|
import { PullWorkspaceUseCase } from './pullWorkspace.js';
|
|
6
6
|
import { CheckWorkspaceStatusUseCase } from './checkWorkspaceStatus.js';
|
|
7
7
|
import { DiscoverPrunableWorkspacesUseCase } from './discoverPrunableWorkspaces.js';
|
|
8
|
+
import { ListWorkspacesWithStatusUseCase } from './listWorkspacesWithStatus.js';
|
|
8
9
|
/**
|
|
9
10
|
* Factory function for creating all use cases with their service dependencies.
|
|
10
11
|
* Use cases orchestrate workflows by coordinating multiple services.
|
|
@@ -18,5 +19,6 @@ export function createUseCases(services) {
|
|
|
18
19
|
pullWorkspace: new PullWorkspaceUseCase(services.workspaceDir, services.git, services.parallel),
|
|
19
20
|
checkWorkspaceStatus: new CheckWorkspaceStatusUseCase(services.workspaceDir, services.fetch, services.status),
|
|
20
21
|
discoverPrunableWorkspaces: new DiscoverPrunableWorkspacesUseCase(services.workspaceDir, services.fetch, services.status, services.git),
|
|
22
|
+
listWorkspacesWithStatus: new ListWorkspacesWithStatusUseCase(services.workspaceDir, services.fetch, services.status),
|
|
21
23
|
};
|
|
22
24
|
}
|