worktree-flow 0.0.18 → 0.0.19

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
@@ -87,7 +87,7 @@ Drop a workspace and all its worktrees. Fetches latest, checks for uncommitted c
87
87
 
88
88
  ### `flow prune`
89
89
 
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.
90
+ Remove workspaces interactively. Displays full workspace status (identical to `flow list`), then excludes any workspaces with uncommitted changes or errors from the selection prompt. Resolve issues before pruning those workspaces.
91
91
 
92
92
  ### `flow fetch [name]`
93
93
 
@@ -1,41 +1,46 @@
1
+ import path from 'node:path';
1
2
  import chalk from 'chalk';
2
3
  import confirm from '@inquirer/confirm';
3
4
  import { createServices } from '../lib/services.js';
4
5
  import { createUseCases } from '../usecases/usecases.js';
5
- import { StatusService } from '../lib/status.js';
6
6
  import { resolveWorkspace } from '../lib/workspaceResolver.js';
7
+ import { logStatusFetching, logStatus } from './helpers.js';
8
+ import { StatusService } from '../lib/status.js';
9
+ import { WorkspaceHasIssuesError } from '../lib/errors.js';
7
10
  export async function runDrop(branchName, useCases, services, deps) {
8
11
  const { sourcePath } = services.config.getRequired();
9
12
  const config = services.config.load();
10
13
  const { workspacePath, displayName: branchNameForDisplay } = resolveWorkspace(branchName, services.workspaceDir, services.config, services.process);
11
- services.console.log(`Checking workspace: ${chalk.cyan(workspacePath)}`);
12
14
  // Get worktree dirs to show what will be removed
13
15
  const worktreeDirs = services.workspaceDir.getWorktreeDirs(workspacePath);
14
16
  // Display status check if worktrees exist
15
17
  if (worktreeDirs.length > 0) {
18
+ const workspaceName = path.basename(workspacePath);
19
+ const repoCount = worktreeDirs.length;
20
+ // Phase 1: Show header with fetching indicator
21
+ const loadingLines = logStatusFetching('Workspace:', [{ name: workspaceName, repoCount }], services.console);
22
+ // Fetch workspace repos (silently)
16
23
  await useCases.fetchWorkspaceRepos.execute({
17
24
  workspacePath,
18
25
  sourcePath,
19
26
  fetchCacheTtlSeconds: config.fetchCacheTtlSeconds,
27
+ silent: true,
28
+ });
29
+ const statusResult = await useCases.checkWorkspaceStatus.execute({
30
+ workspacePath,
20
31
  });
21
- services.console.log(`\nChecking for uncommitted changes...`);
22
32
  // Load workspace config to get per-repo base branches
23
33
  const workspaceConfig = services.workspaceConfig.load(workspacePath);
24
- const getBaseBranch = (repoName) => workspaceConfig.baseBranches[repoName] || 'master';
25
- const results = await services.status.checkAllWorktrees(worktreeDirs, getBaseBranch);
26
- for (const { repoName, status } of results) {
27
- const baseBranch = getBaseBranch(repoName);
28
- const message = StatusService.getStatusMessage(status, baseBranch);
29
- if (StatusService.hasIssues(status)) {
30
- services.console.log(`${repoName}... ${chalk.red(message)}`);
31
- }
32
- else {
33
- services.console.log(`${repoName}... ${chalk.green(message)}`);
34
- }
34
+ // Phase 2: Clear Phase 1 lines and re-render with full status
35
+ logStatus('Workspace:', [{ name: workspaceName, path: workspacePath, repoCount, isActive: false, statuses: statusResult.statuses }], loadingLines, (_, repoName) => workspaceConfig.baseBranches[repoName] || 'master', services.console);
36
+ // Block if any repos have uncommitted changes — user must resolve them first
37
+ const reposWithIssues = statusResult.statuses.filter(({ status }) => StatusService.hasIssues(status));
38
+ if (reposWithIssues.length > 0) {
39
+ throw new WorkspaceHasIssuesError(`${reposWithIssues.length} repo(s) have uncommitted changes or errors. Resolve them before dropping.`);
35
40
  }
36
41
  }
37
42
  else {
38
- services.console.log('No worktrees found in workspace.');
43
+ services.console.log('\nNo worktrees found in workspace.');
39
44
  }
40
45
  // Show what will be removed
41
46
  services.console.log(`\n${chalk.yellow('This will remove:')}`);
@@ -71,34 +71,3 @@ export function buildRepoCheckboxChoices(repos, services, preSelected, createSep
71
71
  }
72
72
  return choices;
73
73
  }
74
- /**
75
- * Get a human-readable status indicator for a workspace based on its worktree statuses.
76
- */
77
- export function getStatusIndicator(workspace) {
78
- const hasUncommitted = workspace.statuses.some(s => s.status.type === 'uncommitted');
79
- const hasAhead = workspace.statuses.some(s => s.status.type === 'ahead');
80
- const hasBehind = workspace.statuses.some(s => s.status.type === 'behind');
81
- const hasDiverged = workspace.statuses.some(s => s.status.type === 'diverged');
82
- const hasError = workspace.statuses.some(s => s.status.type === 'error');
83
- if (hasUncommitted) {
84
- return chalk.yellow('uncommitted');
85
- }
86
- else if (hasDiverged) {
87
- return chalk.red('diverged');
88
- }
89
- else if (hasAhead && hasBehind) {
90
- return chalk.yellow('ahead');
91
- }
92
- else if (hasAhead) {
93
- return chalk.yellow('ahead');
94
- }
95
- else if (hasBehind) {
96
- return chalk.blue('behind');
97
- }
98
- else if (hasError) {
99
- return chalk.red('error');
100
- }
101
- else {
102
- return chalk.green('clean');
103
- }
104
- }
@@ -3,7 +3,8 @@ import checkbox from '@inquirer/checkbox';
3
3
  import confirm from '@inquirer/confirm';
4
4
  import { createServices } from '../lib/services.js';
5
5
  import { createUseCases } from '../usecases/usecases.js';
6
- import { getStatusIndicator } from './helpers.js';
6
+ import { logStatusFetching, logStatus } from './helpers.js';
7
+ import { StatusService } from '../lib/status.js';
7
8
  export async function runPrune(useCases, services, deps) {
8
9
  const { sourcePath, destPath } = services.config.getRequired();
9
10
  const config = services.config.load();
@@ -14,11 +15,14 @@ export async function runPrune(useCases, services, deps) {
14
15
  services.console.log('No workspaces found.');
15
16
  services.process.exit(0);
16
17
  }
18
+ // Phase 1: Show basic list with fetching indicator
19
+ const loadingLines = logStatusFetching('Workspaces:', basicWorkspaces, services.console);
17
20
  // Fetch repos used across all workspaces
18
21
  await useCases.fetchUsedRepos.execute({
19
22
  destPath,
20
23
  sourcePath,
21
24
  fetchCacheTtlSeconds: config.fetchCacheTtlSeconds,
25
+ silent: true,
22
26
  });
23
27
  // Get full status for all workspaces
24
28
  const result = await useCases.listWorkspacesWithStatus.execute({
@@ -26,12 +30,31 @@ export async function runPrune(useCases, services, deps) {
26
30
  sourcePath,
27
31
  cwd,
28
32
  });
29
- // Format choices for checkbox prompt
30
- const choices = result.workspaces.map(workspace => {
31
- const statusIndicator = getStatusIndicator(workspace);
33
+ // Phase 2: Clear Phase 1 and re-print with full status (same as `list` command)
34
+ logStatus('Workspaces:', result.workspaces, loadingLines, (wsPath, repoName) => services.workspaceConfig.load(wsPath).baseBranches[repoName] || 'master', services.console);
35
+ // Partition workspaces into prunable (no issues) and skipped (has issues)
36
+ const prunableWorkspaces = result.workspaces.filter(ws => !ws.statuses.some(({ status }) => StatusService.hasIssues(status)));
37
+ const skippedWorkspaces = result.workspaces.filter(ws => ws.statuses.some(({ status }) => StatusService.hasIssues(status)));
38
+ // Log skipped workspaces with reason
39
+ for (const ws of skippedWorkspaces) {
40
+ const hasUncommitted = ws.statuses.some(s => s.status.type === 'uncommitted');
41
+ const hasError = ws.statuses.some(s => s.status.type === 'error');
42
+ const reason = hasUncommitted ? 'uncommitted changes' : hasError ? 'errors' : 'issues';
43
+ services.console.log(`${chalk.yellow('⚠')} Skipping ${chalk.cyan(ws.name)} (${reason})`);
44
+ }
45
+ // Newline if there were skipped workspaces
46
+ if (skippedWorkspaces.length)
47
+ console.log('');
48
+ // If no prunable workspaces remain, exit
49
+ if (prunableWorkspaces.length === 0) {
50
+ services.console.log('\nAll workspaces have uncommitted changes or errors. Resolve them before pruning.');
51
+ services.process.exit(0);
52
+ }
53
+ // Format choices for checkbox prompt (only prunable workspaces)
54
+ const choices = prunableWorkspaces.map(workspace => {
32
55
  const repoCount = chalk.dim(`(${workspace.repoCount} repo${workspace.repoCount === 1 ? '' : 's'})`);
33
56
  return {
34
- name: `${chalk.cyan(workspace.name)} ${repoCount} ${statusIndicator}`,
57
+ name: `${chalk.cyan(workspace.name)} ${repoCount}`,
35
58
  value: workspace.name,
36
59
  };
37
60
  });
@@ -46,7 +69,7 @@ export async function runPrune(useCases, services, deps) {
46
69
  services.process.exit(0);
47
70
  }
48
71
  // Get full workspace objects for selected items
49
- const selectedWorkspaces = result.workspaces.filter(w => selected.includes(w.name));
72
+ const selectedWorkspaces = prunableWorkspaces.filter(w => selected.includes(w.name));
50
73
  // Confirm deletion
51
74
  const confirmed = await deps.confirm({
52
75
  message: `Are you sure you want to prune ${selected.length} workspace(s)?`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktree-flow",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Manage git worktrees across a poly-repo environment",
5
5
  "type": "module",
6
6
  "bin": {