vde-worktree 0.0.13 → 0.0.15

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/dist/index.mjs CHANGED
@@ -1200,7 +1200,11 @@ const RESERVED_FZF_ARGS = new Set([
1200
1200
  "height",
1201
1201
  "border"
1202
1202
  ]);
1203
+ const ANSI_ESCAPE_SEQUENCE_PATTERN = String.raw`\u001B\[[0-?]*[ -/]*[@-~]`;
1204
+ const ANSI_ESCAPE_SEQUENCE_REGEX = new RegExp(ANSI_ESCAPE_SEQUENCE_PATTERN, "g");
1203
1205
  const sanitizeCandidate = (value) => value.replace(/[\r\n]+/g, " ").trim();
1206
+ const stripAnsi = (value) => value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, "");
1207
+ const stripTrailingNewlines = (value) => value.replace(/[\r\n]+$/g, "");
1204
1208
  const buildFzfInput = (candidates) => {
1205
1209
  return candidates.map((candidate) => sanitizeCandidate(candidate)).filter((candidate) => candidate.length > 0).join("\n");
1206
1210
  };
@@ -1256,14 +1260,14 @@ const selectPathWithFzf = async ({ candidates, prompt = "worktree> ", fzfExtraAr
1256
1260
  });
1257
1261
  const input = buildFzfInput(candidates);
1258
1262
  if (input.length === 0) throw new Error("All candidates are empty after sanitization");
1259
- const candidateSet = new Set(input.split("\n"));
1263
+ const candidateSet = new Set(input.split("\n").map((candidate) => stripAnsi(candidate)));
1260
1264
  try {
1261
- const selectedPath = (await runFzf({
1265
+ const selectedPath = stripAnsi(stripTrailingNewlines((await runFzf({
1262
1266
  args,
1263
1267
  input,
1264
1268
  cwd,
1265
1269
  env
1266
- })).stdout.trim();
1270
+ })).stdout));
1267
1271
  if (selectedPath.length === 0) return { status: "cancelled" };
1268
1272
  if (!candidateSet.has(selectedPath)) throw new Error("fzf returned a value that is not in the candidate list");
1269
1273
  return {
@@ -1436,9 +1440,9 @@ const colorizeListTableLine = ({ line, theme }) => {
1436
1440
  const segments = line.split("│");
1437
1441
  if (segments.length < 3) return line;
1438
1442
  const cells = segments.slice(1, -1);
1439
- if (cells.length !== 5) return line;
1443
+ if (cells.length !== 7) return line;
1440
1444
  const headers = cells.map((cell) => cell.trim());
1441
- if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "path") {
1445
+ if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "ahead" && headers[5] === "behind" && headers[6] === "path") {
1442
1446
  const nextCells = cells.map((cell) => colorizeCellContent({
1443
1447
  cell,
1444
1448
  color: theme.header
@@ -1453,13 +1457,21 @@ const colorizeListTableLine = ({ line, theme }) => {
1453
1457
  const dirtyCell = cells[1];
1454
1458
  const mergedCell = cells[2];
1455
1459
  const lockedCell = cells[3];
1456
- const pathCell = cells[4];
1460
+ const aheadCell = cells[4];
1461
+ const behindCell = cells[5];
1462
+ const pathCell = cells[6];
1457
1463
  const branchColor = branchCell.includes("(detached)") === true ? theme.branchDetached : branchCell.trimStart().startsWith("*") ? theme.branchCurrent : theme.branch;
1458
1464
  const dirtyTrimmed = dirtyCell.trim();
1459
1465
  const dirtyColor = dirtyTrimmed === "dirty" ? theme.dirty : dirtyTrimmed === "clean" ? theme.clean : theme.value;
1460
1466
  const mergedTrimmed = mergedCell.trim();
1461
1467
  const mergedColor = mergedTrimmed === "merged" ? theme.merged : mergedTrimmed === "unmerged" ? theme.unmerged : mergedTrimmed === "-" ? theme.base : theme.unknown;
1462
1468
  const lockedColor = lockedCell.trim() === "locked" ? theme.locked : theme.muted;
1469
+ const aheadTrimmed = aheadCell.trim();
1470
+ const aheadValue = Number.parseInt(aheadTrimmed, 10);
1471
+ const aheadColor = aheadTrimmed === "-" ? theme.muted : Number.isNaN(aheadValue) ? theme.value : aheadValue > 0 ? theme.unmerged : aheadValue === 0 ? theme.merged : theme.unknown;
1472
+ const behindTrimmed = behindCell.trim();
1473
+ const behindValue = Number.parseInt(behindTrimmed, 10);
1474
+ const behindColor = behindTrimmed === "-" ? theme.muted : Number.isNaN(behindValue) ? theme.value : behindValue > 0 ? theme.unknown : behindValue === 0 ? theme.merged : theme.unknown;
1463
1475
  const nextCells = [
1464
1476
  colorizeCellContent({
1465
1477
  cell: branchCell,
@@ -1477,6 +1489,14 @@ const colorizeListTableLine = ({ line, theme }) => {
1477
1489
  cell: lockedCell,
1478
1490
  color: lockedColor
1479
1491
  }),
1492
+ colorizeCellContent({
1493
+ cell: aheadCell,
1494
+ color: aheadColor
1495
+ }),
1496
+ colorizeCellContent({
1497
+ cell: behindCell,
1498
+ color: behindColor
1499
+ }),
1480
1500
  colorizeCellContent({
1481
1501
  cell: pathCell,
1482
1502
  color: theme.path
@@ -1505,7 +1525,7 @@ const commandHelpEntries = [
1505
1525
  name: "list",
1506
1526
  usage: "vw list [--json]",
1507
1527
  summary: "List worktrees with status metadata.",
1508
- details: ["Includes branch, path, dirty, lock, merged, and upstream fields."]
1528
+ details: ["Table output includes branch, path, dirty, lock, merged, and ahead/behind vs base branch.", "JSON output includes upstream metadata fields."]
1509
1529
  },
1510
1530
  {
1511
1531
  name: "status",
@@ -2338,6 +2358,37 @@ const formatMergedColor = ({ mergedState, theme }) => {
2338
2358
  if (normalized === "base") return theme.base(mergedState);
2339
2359
  return theme.unknown(mergedState);
2340
2360
  };
2361
+ const formatListUpstreamCount = (value) => {
2362
+ if (value === null) return "-";
2363
+ return String(value);
2364
+ };
2365
+ const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
2366
+ if (baseBranch === null) return {
2367
+ ahead: null,
2368
+ behind: null
2369
+ };
2370
+ const distance = await runGitCommand({
2371
+ cwd: repoRoot,
2372
+ args: [
2373
+ "rev-list",
2374
+ "--left-right",
2375
+ "--count",
2376
+ `${baseBranch}...${worktree.branch ?? worktree.head}`
2377
+ ],
2378
+ reject: false
2379
+ });
2380
+ if (distance.exitCode !== 0) return {
2381
+ ahead: null,
2382
+ behind: null
2383
+ };
2384
+ const [behindRaw, aheadRaw] = distance.stdout.trim().split(/\s+/);
2385
+ const behind = Number.parseInt(behindRaw ?? "", 10);
2386
+ const ahead = Number.parseInt(aheadRaw ?? "", 10);
2387
+ return {
2388
+ ahead: Number.isNaN(ahead) ? null : ahead,
2389
+ behind: Number.isNaN(behind) ? null : behind
2390
+ };
2391
+ };
2341
2392
  const padToDisplayWidth = ({ value, width }) => {
2342
2393
  const visibleLength = stringWidth(value);
2343
2394
  if (visibleLength >= width) return value;
@@ -2906,17 +2957,26 @@ const createCli = (options = {}) => {
2906
2957
  "dirty",
2907
2958
  "merged",
2908
2959
  "locked",
2960
+ "ahead",
2961
+ "behind",
2909
2962
  "path"
2910
- ], ...snapshot.worktrees.map((worktree) => {
2963
+ ], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
2964
+ const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
2965
+ repoRoot,
2966
+ baseBranch: snapshot.baseBranch,
2967
+ worktree
2968
+ });
2911
2969
  const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
2912
2970
  return [
2913
2971
  `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
2914
2972
  worktree.dirty ? "dirty" : "clean",
2915
2973
  mergedState,
2916
2974
  worktree.locked.value ? "locked" : "-",
2975
+ formatListUpstreamCount(distanceFromBase.ahead),
2976
+ formatListUpstreamCount(distanceFromBase.behind),
2917
2977
  formatDisplayPath(worktree.path)
2918
2978
  ];
2919
- })], {
2979
+ }))], {
2920
2980
  border: getBorderCharacters("norc"),
2921
2981
  drawHorizontalLine: (lineIndex, rowCount) => {
2922
2982
  return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;