vde-worktree 0.0.3 → 0.0.5

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
@@ -5,10 +5,11 @@ import { access, appendFile, chmod, cp, mkdir, open, readFile, readdir, rename,
5
5
  import { homedir, hostname } from "node:os";
6
6
  import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import chalk, { Chalk } from "chalk";
8
9
  import { parseArgs } from "citty";
9
10
  import { execa } from "execa";
10
11
  import stringWidth from "string-width";
11
- import chalk from "chalk";
12
+ import { getBorderCharacters, table } from "table";
12
13
 
13
14
  //#region src/core/constants.ts
14
15
  const SCHEMA_VERSION = 1;
@@ -1001,7 +1002,7 @@ const RESERVED_FZF_ARGS = new Set([
1001
1002
  "height",
1002
1003
  "border"
1003
1004
  ]);
1004
- const sanitizeCandidate = (value) => value.replace(/[\t\r\n]+/g, " ").trim();
1005
+ const sanitizeCandidate = (value) => value.replace(/[\r\n]+/g, " ").trim();
1005
1006
  const buildFzfInput = (candidates) => {
1006
1007
  return candidates.map((candidate) => sanitizeCandidate(candidate)).filter((candidate) => candidate.length > 0).join("\n");
1007
1008
  };
@@ -1152,11 +1153,149 @@ const loadPackageVersion = (requireFn) => {
1152
1153
  //#region src/cli/index.ts
1153
1154
  const EXIT_CODE_CANCELLED = 130;
1154
1155
  const optionNamesAllowOptionLikeValue = new Set(["fzfArg", "fzf-arg"]);
1156
+ const CD_FZF_EXTRA_ARGS = [
1157
+ "--delimiter= ",
1158
+ "--with-nth=1",
1159
+ "--preview=printf '%b' {3}",
1160
+ "--preview-window=right,60%,wrap",
1161
+ "--ansi"
1162
+ ];
1155
1163
  const COMPLETION_SHELLS = ["zsh", "fish"];
1156
1164
  const COMPLETION_FILE_BY_SHELL = {
1157
1165
  zsh: "zsh/_vw",
1158
1166
  fish: "fish/vw.fish"
1159
1167
  };
1168
+ const CATPPUCCIN_MOCHA = {
1169
+ rosewater: "#f5e0dc",
1170
+ mauve: "#cba6f7",
1171
+ red: "#f38ba8",
1172
+ peach: "#fab387",
1173
+ yellow: "#f9e2af",
1174
+ green: "#a6e3a1",
1175
+ blue: "#89b4fa",
1176
+ lavender: "#b4befe",
1177
+ sapphire: "#74c7ec",
1178
+ text: "#cdd6f4",
1179
+ subtext0: "#a6adc8",
1180
+ overlay0: "#6c7086"
1181
+ };
1182
+ const identityColor = (value) => value;
1183
+ const createCatppuccinTheme = ({ enabled }) => {
1184
+ if (enabled !== true) return {
1185
+ header: identityColor,
1186
+ branch: identityColor,
1187
+ branchCurrent: identityColor,
1188
+ branchDetached: identityColor,
1189
+ dirty: identityColor,
1190
+ clean: identityColor,
1191
+ merged: identityColor,
1192
+ unmerged: identityColor,
1193
+ unknown: identityColor,
1194
+ base: identityColor,
1195
+ locked: identityColor,
1196
+ path: identityColor,
1197
+ muted: identityColor,
1198
+ value: identityColor,
1199
+ previewLabel: identityColor,
1200
+ previewSection: identityColor
1201
+ };
1202
+ const chalk = new Chalk({ level: 3 });
1203
+ const color = (hex) => (value) => chalk.hex(hex)(value);
1204
+ return {
1205
+ header: color(CATPPUCCIN_MOCHA.rosewater),
1206
+ branch: color(CATPPUCCIN_MOCHA.lavender),
1207
+ branchCurrent: color(CATPPUCCIN_MOCHA.mauve),
1208
+ branchDetached: color(CATPPUCCIN_MOCHA.peach),
1209
+ dirty: color(CATPPUCCIN_MOCHA.peach),
1210
+ clean: color(CATPPUCCIN_MOCHA.green),
1211
+ merged: color(CATPPUCCIN_MOCHA.green),
1212
+ unmerged: color(CATPPUCCIN_MOCHA.red),
1213
+ unknown: color(CATPPUCCIN_MOCHA.yellow),
1214
+ base: color(CATPPUCCIN_MOCHA.blue),
1215
+ locked: color(CATPPUCCIN_MOCHA.red),
1216
+ path: color(CATPPUCCIN_MOCHA.sapphire),
1217
+ muted: color(CATPPUCCIN_MOCHA.overlay0),
1218
+ value: color(CATPPUCCIN_MOCHA.text),
1219
+ previewLabel: color(CATPPUCCIN_MOCHA.mauve),
1220
+ previewSection: color(CATPPUCCIN_MOCHA.rosewater)
1221
+ };
1222
+ };
1223
+ const shouldUseAnsiColors = ({ interactive }) => {
1224
+ return interactive === true;
1225
+ };
1226
+ const colorizeCellContent = ({ cell, color }) => {
1227
+ const matched = /^(\s*)(.*?)(\s*)$/.exec(cell);
1228
+ if (matched === null) return cell;
1229
+ const leftPadding = matched[1] ?? "";
1230
+ const content = matched[2] ?? "";
1231
+ const rightPadding = matched[3] ?? "";
1232
+ if (content.length === 0) return cell;
1233
+ return `${leftPadding}${color(content)}${rightPadding}`;
1234
+ };
1235
+ const colorizeListTableLine = ({ line, theme }) => {
1236
+ if (line.startsWith("┌") || line.startsWith("├") || line.startsWith("└")) return theme.muted(line);
1237
+ if (line.startsWith("│") !== true) return line;
1238
+ const segments = line.split("│");
1239
+ if (segments.length < 3) return line;
1240
+ const cells = segments.slice(1, -1);
1241
+ if (cells.length !== 5) return line;
1242
+ const headers = cells.map((cell) => cell.trim());
1243
+ if (headers[0] === "branch" && headers[1] === "dirty" && headers[2] === "merged" && headers[3] === "locked" && headers[4] === "path") {
1244
+ const nextCells = cells.map((cell) => colorizeCellContent({
1245
+ cell,
1246
+ color: theme.header
1247
+ }));
1248
+ return [
1249
+ segments[0],
1250
+ ...nextCells,
1251
+ segments.at(-1) ?? ""
1252
+ ].join("│");
1253
+ }
1254
+ const branchCell = cells[0];
1255
+ const dirtyCell = cells[1];
1256
+ const mergedCell = cells[2];
1257
+ const lockedCell = cells[3];
1258
+ const pathCell = cells[4];
1259
+ const branchColor = branchCell.includes("(detached)") === true ? theme.branchDetached : branchCell.trimStart().startsWith("*") ? theme.branchCurrent : theme.branch;
1260
+ const dirtyTrimmed = dirtyCell.trim();
1261
+ const dirtyColor = dirtyTrimmed === "dirty" ? theme.dirty : dirtyTrimmed === "clean" ? theme.clean : theme.value;
1262
+ const mergedTrimmed = mergedCell.trim();
1263
+ const mergedColor = mergedTrimmed === "merged" ? theme.merged : mergedTrimmed === "unmerged" ? theme.unmerged : mergedTrimmed === "-" ? theme.base : theme.unknown;
1264
+ const lockedColor = lockedCell.trim() === "locked" ? theme.locked : theme.muted;
1265
+ const nextCells = [
1266
+ colorizeCellContent({
1267
+ cell: branchCell,
1268
+ color: branchColor
1269
+ }),
1270
+ colorizeCellContent({
1271
+ cell: dirtyCell,
1272
+ color: dirtyColor
1273
+ }),
1274
+ colorizeCellContent({
1275
+ cell: mergedCell,
1276
+ color: mergedColor
1277
+ }),
1278
+ colorizeCellContent({
1279
+ cell: lockedCell,
1280
+ color: lockedColor
1281
+ }),
1282
+ colorizeCellContent({
1283
+ cell: pathCell,
1284
+ color: theme.path
1285
+ })
1286
+ ];
1287
+ return [
1288
+ segments[0],
1289
+ ...nextCells,
1290
+ segments.at(-1) ?? ""
1291
+ ].join("│");
1292
+ };
1293
+ const colorizeListTable = ({ rendered, theme }) => {
1294
+ return rendered.trimEnd().split("\n").map((line) => colorizeListTableLine({
1295
+ line,
1296
+ theme
1297
+ })).join("\n");
1298
+ };
1160
1299
  const commandHelpEntries = [
1161
1300
  {
1162
1301
  name: "init",
@@ -1407,6 +1546,11 @@ const collectOptionValues = ({ args, optionNames }) => {
1407
1546
  }
1408
1547
  return values;
1409
1548
  };
1549
+ const mergeFzfArgs = ({ defaults, extras }) => {
1550
+ const merged = [...defaults];
1551
+ for (const arg of extras) if (merged.includes(arg) !== true) merged.push(arg);
1552
+ return merged;
1553
+ };
1410
1554
  const toNumberOption = ({ value, optionName }) => {
1411
1555
  if (value === void 0) return;
1412
1556
  if (typeof value !== "string") throw createCliError("INVALID_ARGUMENT", { message: `${optionName} must be a number` });
@@ -1728,10 +1872,123 @@ const formatDisplayPath = (absolutePath) => {
1728
1872
  if (absolutePath.startsWith(`${homeDirectory}${sep}`)) return `~${absolutePath.slice(homeDirectory.length)}`;
1729
1873
  return absolutePath;
1730
1874
  };
1731
- const padEndByWidth = (value, targetWidth) => {
1732
- const width = stringWidth(value);
1733
- if (width >= targetWidth) return value;
1734
- return `${value}${" ".repeat(targetWidth - width)}`;
1875
+ const encodeCdPreviewField = (value) => {
1876
+ return value.replace(/\\/g, "\\\\").split("\x1B").join("\\033").replace(/\t/g, " ").replace(/\r\n?/g, "\n").replace(/\n/g, "\\n");
1877
+ };
1878
+ const formatMergedDisplayState = ({ mergedOverall, isBaseBranch, baseLabel = "base" }) => {
1879
+ if (isBaseBranch) return baseLabel;
1880
+ if (mergedOverall === true) return "merged";
1881
+ if (mergedOverall === false) return "unmerged";
1882
+ return "unknown";
1883
+ };
1884
+ const formatMergedColor = ({ mergedState, theme }) => {
1885
+ const normalized = mergedState.toLowerCase();
1886
+ if (normalized === "merged") return theme.merged(mergedState);
1887
+ if (normalized === "unmerged") return theme.unmerged(mergedState);
1888
+ if (normalized === "base") return theme.base(mergedState);
1889
+ return theme.unknown(mergedState);
1890
+ };
1891
+ const padToDisplayWidth = ({ value, width }) => {
1892
+ const visibleLength = stringWidth(value);
1893
+ if (visibleLength >= width) return value;
1894
+ return `${value}${" ".repeat(width - visibleLength)}`;
1895
+ };
1896
+ const buildCdBranchLabel = ({ worktree, currentWorktreeRoot }) => {
1897
+ return `${worktree.path === currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`;
1898
+ };
1899
+ const buildCdStateSummary = ({ worktree, isBaseBranch, theme }) => {
1900
+ const dirtyLabel = worktree.dirty ? "DIRTY" : "CLEAN";
1901
+ const mergedLabel = formatMergedDisplayState({
1902
+ mergedOverall: worktree.merged.overall,
1903
+ isBaseBranch
1904
+ }).toUpperCase();
1905
+ const lockLabel = worktree.locked.value ? "LOCK" : "OPEN";
1906
+ const dirtyBadge = (worktree.dirty ? theme.unmerged : theme.clean)(padToDisplayWidth({
1907
+ value: dirtyLabel,
1908
+ width: 5
1909
+ }));
1910
+ const mergedBadge = formatMergedColor({
1911
+ mergedState: padToDisplayWidth({
1912
+ value: mergedLabel,
1913
+ width: 8
1914
+ }),
1915
+ theme
1916
+ });
1917
+ const lockBadge = (worktree.locked.value ? theme.locked : theme.muted)(padToDisplayWidth({
1918
+ value: lockLabel,
1919
+ width: 4
1920
+ }));
1921
+ return `${dirtyBadge} ${theme.muted("|")} ${mergedBadge} ${theme.muted("|")} ${lockBadge}`;
1922
+ };
1923
+ const buildCdPreviewText = ({ worktree, baseBranch, theme }) => {
1924
+ const isBaseBranch = typeof worktree.branch === "string" && baseBranch !== null && worktree.branch === baseBranch;
1925
+ const branchLabel = worktree.branch === null ? theme.branchDetached("(detached)") : isBaseBranch ? theme.base(worktree.branch) : theme.branch(worktree.branch);
1926
+ const pathLabel = theme.path(formatDisplayPath(worktree.path));
1927
+ const dirtyValue = worktree.dirty ? theme.unmerged("[DIRTY]") : theme.merged("[CLEAN]");
1928
+ const lockedValue = worktree.locked.value ? theme.locked("[LOCKED]") : theme.clean("[OPEN]");
1929
+ const mergedValue = formatMergedColor({
1930
+ mergedState: formatMergedDisplayState({
1931
+ mergedOverall: worktree.merged.overall,
1932
+ isBaseBranch
1933
+ }).toUpperCase(),
1934
+ theme
1935
+ });
1936
+ const remoteValue = worktree.upstream.remote === null ? theme.muted("none") : theme.value(worktree.upstream.remote ?? "none");
1937
+ const aheadValue = worktree.upstream.ahead === null ? theme.unknown("UNKNOWN") : worktree.upstream.ahead > 0 ? theme.unmerged(String(worktree.upstream.ahead)) : theme.merged("0");
1938
+ const behindValue = worktree.upstream.behind === null ? theme.unknown("UNKNOWN") : worktree.upstream.behind > 0 ? theme.unknown(String(worktree.upstream.behind)) : theme.merged("0");
1939
+ const divider = theme.muted("----------------------------------------");
1940
+ const lines = [
1941
+ theme.previewSection("WORKTREE"),
1942
+ divider,
1943
+ ` ${theme.previewLabel("Branch ")}: ${branchLabel}`,
1944
+ ` ${theme.previewLabel("Path ")}: ${pathLabel}`,
1945
+ "",
1946
+ theme.previewSection("STATUS"),
1947
+ divider,
1948
+ ` ${theme.previewLabel("Dirty ")}: ${dirtyValue}`,
1949
+ ` ${theme.previewLabel("Locked ")}: ${lockedValue}`,
1950
+ ` ${theme.previewLabel("Merged ")}: ${mergedValue}`,
1951
+ ` ${theme.previewLabel("Remote ")}: ${remoteValue}`,
1952
+ ` ${theme.previewLabel("Ahead ")}: ${aheadValue}`,
1953
+ ` ${theme.previewLabel("Behind ")}: ${behindValue}`
1954
+ ];
1955
+ if (worktree.locked.value) {
1956
+ lines.push("");
1957
+ lines.push(theme.previewSection("LOCK"));
1958
+ lines.push(divider);
1959
+ if (typeof worktree.locked.reason === "string" && worktree.locked.reason.length > 0) lines.push(` ${theme.previewLabel("Reason ")}: ${theme.value(worktree.locked.reason)}`);
1960
+ if (typeof worktree.locked.owner === "string" && worktree.locked.owner.length > 0) lines.push(` ${theme.previewLabel("Owner ")}: ${theme.value(worktree.locked.owner)}`);
1961
+ }
1962
+ return lines.join("\n");
1963
+ };
1964
+ const buildCdCandidateLine = ({ worktree, baseBranch, theme, currentWorktreeRoot, branchColumnWidth }) => {
1965
+ const isBaseBranch = typeof worktree.branch === "string" && baseBranch !== null && worktree.branch === baseBranch;
1966
+ const branchLabelPadded = padToDisplayWidth({
1967
+ value: buildCdBranchLabel({
1968
+ worktree,
1969
+ currentWorktreeRoot
1970
+ }),
1971
+ width: branchColumnWidth
1972
+ });
1973
+ const isCurrent = worktree.path === currentWorktreeRoot;
1974
+ return [
1975
+ `${worktree.branch === null ? theme.branchDetached(branchLabelPadded) : isCurrent ? theme.branchCurrent(branchLabelPadded) : isBaseBranch ? theme.base(branchLabelPadded) : theme.branch(branchLabelPadded)} ${buildCdStateSummary({
1976
+ worktree,
1977
+ isBaseBranch,
1978
+ theme
1979
+ })}`,
1980
+ worktree.path,
1981
+ encodeCdPreviewField(buildCdPreviewText({
1982
+ worktree,
1983
+ baseBranch,
1984
+ theme
1985
+ }))
1986
+ ].join(" ");
1987
+ };
1988
+ const resolveCdSelectionPath = (selectedLine) => {
1989
+ const rawPath = selectedLine.split(" ")[1];
1990
+ if (typeof rawPath === "string" && rawPath.length > 0) return rawPath;
1991
+ return selectedLine;
1735
1992
  };
1736
1993
  const containsBranch = ({ branch, worktrees }) => {
1737
1994
  return worktrees.some((worktree) => worktree.branch === branch);
@@ -2169,16 +2426,32 @@ const createCli = (options = {}) => {
2169
2426
  })));
2170
2427
  return EXIT_CODE.OK;
2171
2428
  }
2172
- const rows = snapshot.worktrees.map((worktree) => {
2173
- return {
2174
- branch: worktree.branch ?? "(detached)",
2175
- path: formatDisplayPath(worktree.path)
2176
- };
2429
+ const theme = createCatppuccinTheme({ enabled: shouldUseAnsiColors({ interactive: runtime.isInteractive }) });
2430
+ const colorized = colorizeListTable({
2431
+ rendered: table([[
2432
+ "branch",
2433
+ "dirty",
2434
+ "merged",
2435
+ "locked",
2436
+ "path"
2437
+ ], ...snapshot.worktrees.map((worktree) => {
2438
+ const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
2439
+ return [
2440
+ `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
2441
+ worktree.dirty ? "dirty" : "clean",
2442
+ mergedState,
2443
+ worktree.locked.value ? "locked" : "-",
2444
+ formatDisplayPath(worktree.path)
2445
+ ];
2446
+ })], {
2447
+ border: getBorderCharacters("norc"),
2448
+ drawHorizontalLine: (lineIndex, rowCount) => {
2449
+ return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
2450
+ }
2451
+ }),
2452
+ theme
2177
2453
  });
2178
- const branchWidth = rows.reduce((max, row) => {
2179
- return Math.max(max, stringWidth(row.branch));
2180
- }, 0);
2181
- for (const row of rows) stdout(`${padEndByWidth(row.branch, branchWidth)} ${row.path}`);
2454
+ for (const line of colorized.split("\n")) stdout(line);
2182
2455
  return EXIT_CODE.OK;
2183
2456
  }
2184
2457
  if (command === "status") {
@@ -3285,15 +3558,33 @@ const createCli = (options = {}) => {
3285
3558
  min: 0,
3286
3559
  max: 0
3287
3560
  });
3288
- const candidates = (await collectWorktreeSnapshot(repoRoot)).worktrees.map((worktree) => worktree.path);
3561
+ const snapshot = await collectWorktreeSnapshot(repoRoot);
3562
+ const theme = createCatppuccinTheme({ enabled: shouldUseAnsiColors({ interactive: runtime.isInteractive }) });
3563
+ const branchColumnWidth = snapshot.worktrees.reduce((maxWidth, worktree) => {
3564
+ const label = buildCdBranchLabel({
3565
+ worktree,
3566
+ currentWorktreeRoot: repoContext.currentWorktreeRoot
3567
+ });
3568
+ return Math.max(maxWidth, stringWidth(label));
3569
+ }, 0);
3570
+ const candidates = snapshot.worktrees.map((worktree) => buildCdCandidateLine({
3571
+ worktree,
3572
+ baseBranch: snapshot.baseBranch,
3573
+ theme,
3574
+ currentWorktreeRoot: repoContext.currentWorktreeRoot,
3575
+ branchColumnWidth
3576
+ }));
3289
3577
  if (candidates.length === 0) throw createCliError("WORKTREE_NOT_FOUND", { message: "No worktree candidates found" });
3290
3578
  const promptValue = readStringOption(parsedArgsRecord, "prompt");
3291
3579
  const selection = await selectPathWithFzf$1({
3292
3580
  candidates,
3293
3581
  prompt: typeof promptValue === "string" && promptValue.length > 0 ? promptValue : "worktree> ",
3294
- fzfExtraArgs: collectOptionValues({
3295
- args: beforeDoubleDash,
3296
- optionNames: ["fzfArg", "fzf-arg"]
3582
+ fzfExtraArgs: mergeFzfArgs({
3583
+ defaults: CD_FZF_EXTRA_ARGS,
3584
+ extras: collectOptionValues({
3585
+ args: beforeDoubleDash,
3586
+ optionNames: ["fzfArg", "fzf-arg"]
3587
+ })
3297
3588
  }),
3298
3589
  cwd: repoRoot,
3299
3590
  isInteractive: () => runtime.isInteractive || process.stderr.isTTY === true
@@ -3303,16 +3594,17 @@ const createCli = (options = {}) => {
3303
3594
  throw error;
3304
3595
  });
3305
3596
  if (selection.status === "cancelled") return EXIT_CODE_CANCELLED;
3597
+ const selectedPath = resolveCdSelectionPath(selection.path);
3306
3598
  if (runtime.json) {
3307
3599
  stdout(JSON.stringify(buildJsonSuccess({
3308
3600
  command,
3309
3601
  status: "ok",
3310
3602
  repoRoot,
3311
- details: { path: selection.path }
3603
+ details: { path: selectedPath }
3312
3604
  })));
3313
3605
  return EXIT_CODE.OK;
3314
3606
  }
3315
- stdout(selection.path);
3607
+ stdout(selectedPath);
3316
3608
  return EXIT_CODE.OK;
3317
3609
  }
3318
3610
  throw createCliError("UNKNOWN_COMMAND", { message: `Unknown command: ${command}` });