vde-worktree 0.0.4 → 0.0.6

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";
11
+ import stringWidth from "string-width";
10
12
  import { getBorderCharacters, table } from "table";
11
- import chalk from "chalk";
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,6 +1872,124 @@ const formatDisplayPath = (absolutePath) => {
1728
1872
  if (absolutePath.startsWith(`${homeDirectory}${sep}`)) return `~${absolutePath.slice(homeDirectory.length)}`;
1729
1873
  return absolutePath;
1730
1874
  };
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;
1992
+ };
1731
1993
  const containsBranch = ({ branch, worktrees }) => {
1732
1994
  return worktrees.some((worktree) => worktree.branch === branch);
1733
1995
  };
@@ -2164,28 +2426,32 @@ const createCli = (options = {}) => {
2164
2426
  })));
2165
2427
  return EXIT_CODE.OK;
2166
2428
  }
2167
- const rendered = table([[
2168
- "branch",
2169
- "dirty",
2170
- "merged",
2171
- "locked",
2172
- "path"
2173
- ], ...snapshot.worktrees.map((worktree) => {
2174
- const mergedState = (worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch) === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
2175
- return [
2176
- `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
2177
- worktree.dirty ? "dirty" : "clean",
2178
- mergedState,
2179
- worktree.locked.value ? "locked" : "-",
2180
- formatDisplayPath(worktree.path)
2181
- ];
2182
- })], {
2183
- border: getBorderCharacters("norc"),
2184
- drawHorizontalLine: (lineIndex, rowCount) => {
2185
- return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
2186
- }
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
2187
2453
  });
2188
- for (const line of rendered.trimEnd().split("\n")) stdout(line);
2454
+ for (const line of colorized.split("\n")) stdout(line);
2189
2455
  return EXIT_CODE.OK;
2190
2456
  }
2191
2457
  if (command === "status") {
@@ -2581,7 +2847,7 @@ const createCli = (options = {}) => {
2581
2847
  if (parsedArgs.apply === true && parsedArgs.dryRun === true) throw createCliError("INVALID_ARGUMENT", { message: "Cannot use --apply and --dry-run together" });
2582
2848
  const dryRun = parsedArgs.apply !== true;
2583
2849
  const execute = async () => {
2584
- const candidates = (await collectWorktreeSnapshot(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) => worktree.dirty === false).filter((worktree) => worktree.locked.value === false).filter((worktree) => worktree.merged.overall === true).filter((worktree) => worktree.upstream.ahead === 0).map((worktree) => worktree.branch);
2850
+ const candidates = (await collectWorktreeSnapshot(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) => worktree.dirty === false).filter((worktree) => worktree.locked.value === false).filter((worktree) => worktree.merged.byAncestry === true).map((worktree) => worktree.branch);
2585
2851
  if (dryRun) return {
2586
2852
  deleted: [],
2587
2853
  candidates,
@@ -3292,15 +3558,33 @@ const createCli = (options = {}) => {
3292
3558
  min: 0,
3293
3559
  max: 0
3294
3560
  });
3295
- 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
+ }));
3296
3577
  if (candidates.length === 0) throw createCliError("WORKTREE_NOT_FOUND", { message: "No worktree candidates found" });
3297
3578
  const promptValue = readStringOption(parsedArgsRecord, "prompt");
3298
3579
  const selection = await selectPathWithFzf$1({
3299
3580
  candidates,
3300
3581
  prompt: typeof promptValue === "string" && promptValue.length > 0 ? promptValue : "worktree> ",
3301
- fzfExtraArgs: collectOptionValues({
3302
- args: beforeDoubleDash,
3303
- 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
+ })
3304
3588
  }),
3305
3589
  cwd: repoRoot,
3306
3590
  isInteractive: () => runtime.isInteractive || process.stderr.isTTY === true
@@ -3310,16 +3594,17 @@ const createCli = (options = {}) => {
3310
3594
  throw error;
3311
3595
  });
3312
3596
  if (selection.status === "cancelled") return EXIT_CODE_CANCELLED;
3597
+ const selectedPath = resolveCdSelectionPath(selection.path);
3313
3598
  if (runtime.json) {
3314
3599
  stdout(JSON.stringify(buildJsonSuccess({
3315
3600
  command,
3316
3601
  status: "ok",
3317
3602
  repoRoot,
3318
- details: { path: selection.path }
3603
+ details: { path: selectedPath }
3319
3604
  })));
3320
3605
  return EXIT_CODE.OK;
3321
3606
  }
3322
- stdout(selection.path);
3607
+ stdout(selectedPath);
3323
3608
  return EXIT_CODE.OK;
3324
3609
  }
3325
3610
  throw createCliError("UNKNOWN_COMMAND", { message: `Unknown command: ${command}` });