vde-worktree 0.0.17 → 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.ja.md CHANGED
@@ -135,12 +135,16 @@ vw init
135
135
  vw list
136
136
  vw list --json
137
137
  vw list --no-gh
138
+ vw list --full-path
138
139
  ```
139
140
 
140
141
  機能:
141
142
 
142
143
  - Git の porcelain 情報から worktree 一覧を取得
143
144
  - branch/path/dirty/lock/merged/PR/upstream を表示
145
+ - JSON メタデータには non-base branch ごとに `pr.status` と `pr.url` を含む
146
+ - テーブル表示では長い `path` は端末幅に合わせて `…` で省略
147
+ - `--full-path` でテーブル表示の path 省略を無効化
144
148
  - `--no-gh` 指定時は PR 状態判定をスキップ(`pr.status` は `unknown`、`merged.byPR` は `null`)
145
149
  - 対話ターミナルでは Catppuccin 風の ANSI 色で表示
146
150
 
@@ -425,6 +429,7 @@ vw completion zsh --install
425
429
  - `merged.byPR`: GitHub PR merged 判定(`gh`)
426
430
  - `merged.overall`: 最終判定
427
431
  - `pr.status`: PR 状態(`none` / `open` / `merged` / `closed_unmerged` / `unknown`)
432
+ - `pr.url`: branch の最新 PR URL(取得不可時は `null`)
428
433
 
429
434
  `overall` ポリシー:
430
435
 
package/README.md CHANGED
@@ -135,12 +135,16 @@ What it does:
135
135
  vw list
136
136
  vw list --json
137
137
  vw list --no-gh
138
+ vw list --full-path
138
139
  ```
139
140
 
140
141
  What it does:
141
142
 
142
143
  - Lists all worktrees from Git porcelain output
143
144
  - Includes metadata such as branch, path, dirty, lock, merged, PR status, and upstream status
145
+ - JSON metadata includes `pr.status` and `pr.url` for each non-base branch
146
+ - In table output, long `path` values are truncated with `…` to fit terminal width by default
147
+ - Use `--full-path` to disable path truncation in table output
144
148
  - With `--no-gh`, skips PR status checks (`pr.status` becomes `unknown`, `merged.byPR` becomes `null`)
145
149
  - In interactive terminal, uses Catppuccin-style ANSI colors
146
150
 
@@ -425,6 +429,7 @@ Each worktree reports:
425
429
  - `merged.byPR`: PR-based merged check via GitHub CLI
426
430
  - `merged.overall`: final decision
427
431
  - `pr.status`: PR state (`none` / `open` / `merged` / `closed_unmerged` / `unknown`)
432
+ - `pr.url`: latest PR URL for the branch (`null` when unavailable)
428
433
 
429
434
  Overall policy:
430
435
 
@@ -189,6 +189,8 @@ for __vw_bin in vw vde-worktree
189
189
  complete -c $__vw_bin -l json -d "Output machine-readable JSON"
190
190
  complete -c $__vw_bin -l verbose -d "Enable verbose logs"
191
191
  complete -c $__vw_bin -l no-hooks -d "Disable hooks for this run (requires --allow-unsafe)"
192
+ complete -c $__vw_bin -l no-gh -d "Disable GitHub CLI based PR status checks for this run"
193
+ complete -c $__vw_bin -l full-path -d "Disable list table path truncation"
192
194
  complete -c $__vw_bin -l allow-unsafe -d "Explicit unsafe override in non-TTY mode"
193
195
  complete -c $__vw_bin -l strict-post-hooks -d "Fail when post hooks fail"
194
196
  complete -c $__vw_bin -l hook-timeout-ms -r -d "Override hook timeout"
@@ -200,6 +202,7 @@ for __vw_bin in vw vde-worktree
200
202
  complete -c $__vw_bin -n "__fish_seen_subcommand_from path" -a "(__vw_worktree_candidates_with_meta)"
201
203
  complete -c $__vw_bin -n "__fish_seen_subcommand_from switch" -a "(__vw_worktree_candidates_with_meta)"
202
204
  complete -c $__vw_bin -n "__fish_seen_subcommand_from del" -a "(__vw_worktree_candidates_with_meta)"
205
+ complete -c $__vw_bin -n "__fish_seen_subcommand_from list" -l full-path -d "Disable list table path truncation"
203
206
  complete -c $__vw_bin -n "__fish_seen_subcommand_from get" -a "(__vw_remote_branches)"
204
207
  complete -c $__vw_bin -n "__fish_seen_subcommand_from absorb" -a "(__vw_worktree_candidates_with_meta)"
205
208
  complete -c $__vw_bin -n "__fish_seen_subcommand_from unabsorb" -a "(__vw_worktree_candidates_with_meta)"
@@ -263,6 +263,8 @@ _vw() {
263
263
  "--json[Output machine-readable JSON]"
264
264
  "--verbose[Enable verbose logs]"
265
265
  "--no-hooks[Disable hooks for this run]"
266
+ "--no-gh[Disable GitHub CLI based PR status checks for this run]"
267
+ "--full-path[Disable list table path truncation]"
266
268
  "--allow-unsafe[Allow unsafe behavior in non-TTY mode]"
267
269
  "--strict-post-hooks[Fail when post hooks fail]"
268
270
  "--hook-timeout-ms[Override hook timeout]:ms:"
@@ -284,6 +286,10 @@ _vw() {
284
286
  arguments)
285
287
  local current_command="${line[1]:-${words[2]}}"
286
288
  case "${current_command}" in
289
+ list)
290
+ _arguments \
291
+ "--full-path[Disable list table path truncation]"
292
+ ;;
287
293
  status)
288
294
  _arguments \
289
295
  "1:branch:_vw_complete_worktree_branches_with_meta"
package/dist/index.mjs CHANGED
@@ -853,8 +853,11 @@ const toTargetBranches = ({ branches, baseBranch }) => {
853
853
  }
854
854
  return [...uniqueBranches];
855
855
  };
856
- const buildUnknownPrStatusMap = (branches) => {
857
- return new Map(branches.map((branch) => [branch, "unknown"]));
856
+ const buildUnknownPrStateMap = (branches) => {
857
+ return new Map(branches.map((branch) => [branch, {
858
+ status: "unknown",
859
+ url: null
860
+ }]));
858
861
  };
859
862
  const parseUpdatedAtMillis = (value) => {
860
863
  if (typeof value !== "string" || value.length === 0) return Number.NEGATIVE_INFINITY;
@@ -870,7 +873,11 @@ const toPrStatus = (record) => {
870
873
  if (state === "CLOSED") return "closed_unmerged";
871
874
  return "unknown";
872
875
  };
873
- const parsePrStatusByBranch = ({ raw, targetBranches }) => {
876
+ const toPrUrl = (record) => {
877
+ if (typeof record.url === "string" && record.url.length > 0) return record.url;
878
+ return null;
879
+ };
880
+ const parsePrStateByBranch = ({ raw, targetBranches }) => {
874
881
  try {
875
882
  const parsed = JSON.parse(raw);
876
883
  if (Array.isArray(parsed) !== true) return null;
@@ -882,31 +889,43 @@ const parsePrStatusByBranch = ({ raw, targetBranches }) => {
882
889
  if (targetBranchSet.has(record.headRefName) !== true) continue;
883
890
  const updatedAtMillis = parseUpdatedAtMillis(record.updatedAt);
884
891
  const status = toPrStatus(record);
892
+ const url = toPrUrl(record);
885
893
  const current = latestByBranch.get(record.headRefName);
886
894
  if (current === void 0 || updatedAtMillis > current.updatedAtMillis || updatedAtMillis === current.updatedAtMillis && index > current.index) latestByBranch.set(record.headRefName, {
887
895
  updatedAtMillis,
888
896
  index,
889
- status
897
+ status,
898
+ url
890
899
  });
891
900
  }
892
901
  const result = /* @__PURE__ */ new Map();
893
902
  for (const branch of targetBranches) {
894
903
  const latest = latestByBranch.get(branch);
895
- result.set(branch, latest?.status ?? "none");
904
+ if (latest === void 0) {
905
+ result.set(branch, {
906
+ status: "none",
907
+ url: null
908
+ });
909
+ continue;
910
+ }
911
+ result.set(branch, {
912
+ status: latest.status,
913
+ url: latest.url
914
+ });
896
915
  }
897
916
  return result;
898
917
  } catch {
899
918
  return null;
900
919
  }
901
920
  };
902
- const resolvePrStatusByBranchBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
921
+ const resolvePrStateByBranchBatch = async ({ repoRoot, baseBranch, branches, enabled = true, runGh = defaultRunGh }) => {
903
922
  if (baseBranch === null) return /* @__PURE__ */ new Map();
904
923
  const targetBranches = toTargetBranches({
905
924
  branches,
906
925
  baseBranch
907
926
  });
908
927
  if (targetBranches.length === 0) return /* @__PURE__ */ new Map();
909
- if (enabled !== true) return buildUnknownPrStatusMap(targetBranches);
928
+ if (enabled !== true) return buildUnknownPrStateMap(targetBranches);
910
929
  try {
911
930
  const result = await runGh({
912
931
  cwd: repoRoot,
@@ -922,19 +941,19 @@ const resolvePrStatusByBranchBatch = async ({ repoRoot, baseBranch, branches, en
922
941
  "--limit",
923
942
  "1000",
924
943
  "--json",
925
- "headRefName,state,mergedAt,updatedAt"
944
+ "headRefName,state,mergedAt,updatedAt,url"
926
945
  ]
927
946
  });
928
- if (result.exitCode !== 0) return buildUnknownPrStatusMap(targetBranches);
929
- const prStatusByBranch = parsePrStatusByBranch({
947
+ if (result.exitCode !== 0) return buildUnknownPrStateMap(targetBranches);
948
+ const prStatusByBranch = parsePrStateByBranch({
930
949
  raw: result.stdout,
931
950
  targetBranches
932
951
  });
933
- if (prStatusByBranch === null) return buildUnknownPrStatusMap(targetBranches);
952
+ if (prStatusByBranch === null) return buildUnknownPrStateMap(targetBranches);
934
953
  return prStatusByBranch;
935
954
  } catch (error) {
936
- if (error.code === "ENOENT") return buildUnknownPrStatusMap(targetBranches);
937
- return buildUnknownPrStatusMap(targetBranches);
955
+ if (error.code === "ENOENT") return buildUnknownPrStateMap(targetBranches);
956
+ return buildUnknownPrStateMap(targetBranches);
938
957
  }
939
958
  };
940
959
 
@@ -1133,7 +1152,7 @@ const resolveLifecycleFromReflog = async ({ repoRoot, branch, baseBranch }) => {
1133
1152
  divergedHead: latestWorkHead
1134
1153
  };
1135
1154
  };
1136
- const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStatusByBranch }) => {
1155
+ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStateByBranch }) => {
1137
1156
  if (branch === null) return {
1138
1157
  byAncestry: null,
1139
1158
  byPR: null,
@@ -1154,7 +1173,7 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStatus
1154
1173
  if (result.exitCode === 0) byAncestry = true;
1155
1174
  else if (result.exitCode === 1) byAncestry = false;
1156
1175
  }
1157
- const prStatus = branch === baseBranch ? null : prStatusByBranch.get(branch) ?? null;
1176
+ const prStatus = branch === baseBranch ? null : prStateByBranch.get(branch)?.status ?? null;
1158
1177
  let byPR = null;
1159
1178
  if (prStatus === "merged") byPR = true;
1160
1179
  else if (prStatus === "none" || prStatus === "open" || prStatus === "closed_unmerged") byPR = false;
@@ -1208,9 +1227,16 @@ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, prStatus
1208
1227
  })
1209
1228
  };
1210
1229
  };
1211
- const resolvePrState = ({ branch, baseBranch, prStatusByBranch }) => {
1212
- if (branch === null || branch === baseBranch) return { status: null };
1213
- return { status: prStatusByBranch.get(branch) ?? null };
1230
+ const resolveWorktreePrState = ({ branch, baseBranch, prStateByBranch }) => {
1231
+ if (branch === null || branch === baseBranch) return {
1232
+ status: null,
1233
+ url: null
1234
+ };
1235
+ const prState = prStateByBranch.get(branch);
1236
+ return {
1237
+ status: prState?.status ?? null,
1238
+ url: prState?.url ?? null
1239
+ };
1214
1240
  };
1215
1241
  const resolveMergedOverall = ({ byAncestry, byPR, byLifecycle }) => {
1216
1242
  if (byPR === true || byLifecycle === true) return true;
@@ -1258,7 +1284,7 @@ const resolveUpstreamState = async (worktreePath) => {
1258
1284
  remote: upstreamRef.stdout.trim()
1259
1285
  };
1260
1286
  };
1261
- const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStatusByBranch }) => {
1287
+ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStateByBranch }) => {
1262
1288
  const [dirty, locked, merged, upstream] = await Promise.all([
1263
1289
  resolveDirty(worktree.path),
1264
1290
  resolveLockState({
@@ -1270,14 +1296,14 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, prStatusByBranch
1270
1296
  branch: worktree.branch,
1271
1297
  head: worktree.head,
1272
1298
  baseBranch,
1273
- prStatusByBranch
1299
+ prStateByBranch
1274
1300
  }),
1275
1301
  resolveUpstreamState(worktree.path)
1276
1302
  ]);
1277
- const pr = resolvePrState({
1303
+ const pr = resolveWorktreePrState({
1278
1304
  branch: worktree.branch,
1279
1305
  baseBranch,
1280
- prStatusByBranch
1306
+ prStateByBranch
1281
1307
  });
1282
1308
  return {
1283
1309
  branch: worktree.branch,
@@ -1296,7 +1322,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
1296
1322
  listGitWorktrees(repoRoot),
1297
1323
  resolveEnableGh(repoRoot)
1298
1324
  ]);
1299
- const prStatusByBranch = await resolvePrStatusByBranchBatch({
1325
+ const prStateByBranch = await resolvePrStateByBranchBatch({
1300
1326
  repoRoot,
1301
1327
  baseBranch,
1302
1328
  branches: worktrees.map((worktree) => worktree.branch),
@@ -1310,7 +1336,7 @@ const collectWorktreeSnapshot = async (repoRoot, { noGh = false } = {}) => {
1310
1336
  repoRoot,
1311
1337
  worktree,
1312
1338
  baseBranch,
1313
- prStatusByBranch
1339
+ prStateByBranch
1314
1340
  });
1315
1341
  }))
1316
1342
  };
@@ -1488,6 +1514,10 @@ const CD_FZF_EXTRA_ARGS = [
1488
1514
  "--preview-window=right,60%,wrap",
1489
1515
  "--ansi"
1490
1516
  ];
1517
+ const LIST_TABLE_COLUMN_COUNT = 8;
1518
+ const LIST_TABLE_PATH_COLUMN_INDEX = 7;
1519
+ const LIST_TABLE_PATH_MIN_WIDTH = 12;
1520
+ const LIST_TABLE_CELL_HORIZONTAL_PADDING = 2;
1491
1521
  const COMPLETION_SHELLS = ["zsh", "fish"];
1492
1522
  const COMPLETION_FILE_BY_SHELL = {
1493
1523
  zsh: "zsh/_vw",
@@ -1656,9 +1686,14 @@ const commandHelpEntries = [
1656
1686
  },
1657
1687
  {
1658
1688
  name: "list",
1659
- usage: "vw list [--json]",
1689
+ usage: "vw list [--json] [--full-path]",
1660
1690
  summary: "List worktrees with status metadata.",
1661
- details: ["Table output includes branch, path, dirty, lock, merged, PR state, and ahead/behind vs base branch.", "JSON output includes PR and upstream metadata fields."]
1691
+ details: [
1692
+ "Table output includes branch, path, dirty, lock, merged, PR state, and ahead/behind vs base branch.",
1693
+ "By default, long path values are truncated to fit terminal width.",
1694
+ "JSON output includes PR status/url and upstream metadata fields."
1695
+ ],
1696
+ options: ["--full-path"]
1662
1697
  },
1663
1698
  {
1664
1699
  name: "status",
@@ -2484,6 +2519,26 @@ const formatListUpstreamCount = (value) => {
2484
2519
  if (value === null) return "-";
2485
2520
  return String(value);
2486
2521
  };
2522
+ const resolveListColumnContentWidth = ({ rows, columnIndex }) => {
2523
+ return rows.reduce((width, row) => {
2524
+ const cell = row[columnIndex] ?? "";
2525
+ return Math.max(width, stringWidth(cell));
2526
+ }, 0);
2527
+ };
2528
+ const resolveListPathColumnWidth = ({ rows, disablePathTruncation }) => {
2529
+ if (disablePathTruncation) return null;
2530
+ if (process.stdout.isTTY !== true) return null;
2531
+ const terminalColumns = process.stdout.columns;
2532
+ if (typeof terminalColumns !== "number" || Number.isFinite(terminalColumns) !== true || terminalColumns <= 0) return null;
2533
+ const measuredNonPathWidth = Array.from({ length: LIST_TABLE_PATH_COLUMN_INDEX }).map((_, index) => resolveListColumnContentWidth({
2534
+ rows,
2535
+ columnIndex: index
2536
+ })).reduce((sum, width) => sum + width, 0);
2537
+ const borderWidth = LIST_TABLE_COLUMN_COUNT + 1;
2538
+ const paddingWidth = LIST_TABLE_COLUMN_COUNT * LIST_TABLE_CELL_HORIZONTAL_PADDING;
2539
+ const availablePathWidth = Math.floor(terminalColumns) - borderWidth - paddingWidth - measuredNonPathWidth;
2540
+ return Math.max(LIST_TABLE_PATH_MIN_WIDTH, availablePathWidth);
2541
+ };
2487
2542
  const resolveAheadBehindAgainstBaseBranch = async ({ repoRoot, baseBranch, worktree }) => {
2488
2543
  if (baseBranch === null) return {
2489
2544
  ahead: null,
@@ -2642,6 +2697,7 @@ const renderGeneralHelpText = ({ version }) => {
2642
2697
  " --verbose Enable verbose logs.",
2643
2698
  " --no-hooks Disable hooks for this run (requires --allow-unsafe).",
2644
2699
  " --no-gh Disable GitHub CLI based PR status checks for this run.",
2700
+ " --full-path Disable list table path truncation.",
2645
2701
  " --allow-unsafe Explicitly allow unsafe behavior in non-TTY mode.",
2646
2702
  " --hook-timeout-ms <ms> Override hook timeout.",
2647
2703
  " --lock-timeout-ms <ms> Override repository lock timeout.",
@@ -2747,6 +2803,10 @@ const createCli = (options = {}) => {
2747
2803
  description: "Enable GitHub CLI based PR status checks (disable with --no-gh)",
2748
2804
  default: true
2749
2805
  },
2806
+ fullPath: {
2807
+ type: "boolean",
2808
+ description: "Disable list table path truncation"
2809
+ },
2750
2810
  allowUnsafe: {
2751
2811
  type: "boolean",
2752
2812
  description: "Allow unsafe operations"
@@ -3073,43 +3133,53 @@ const createCli = (options = {}) => {
3073
3133
  return EXIT_CODE.OK;
3074
3134
  }
3075
3135
  const theme = createCatppuccinTheme({ enabled: shouldUseAnsiColors({ interactive: runtime.isInteractive }) });
3136
+ const rows = [[
3137
+ "branch",
3138
+ "dirty",
3139
+ "merged",
3140
+ "pr",
3141
+ "locked",
3142
+ "ahead",
3143
+ "behind",
3144
+ "path"
3145
+ ], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
3146
+ const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
3147
+ repoRoot,
3148
+ baseBranch: snapshot.baseBranch,
3149
+ worktree
3150
+ });
3151
+ const isBaseBranch = worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch;
3152
+ const mergedState = isBaseBranch === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
3153
+ const prState = formatPrDisplayState({
3154
+ prStatus: worktree.pr.status,
3155
+ isBaseBranch
3156
+ });
3157
+ return [
3158
+ `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
3159
+ worktree.dirty ? "dirty" : "clean",
3160
+ mergedState,
3161
+ prState,
3162
+ worktree.locked.value ? "locked" : "-",
3163
+ formatListUpstreamCount(distanceFromBase.ahead),
3164
+ formatListUpstreamCount(distanceFromBase.behind),
3165
+ formatDisplayPath(worktree.path)
3166
+ ];
3167
+ }))];
3168
+ const pathColumnWidth = resolveListPathColumnWidth({
3169
+ rows,
3170
+ disablePathTruncation: parsedArgs.fullPath === true
3171
+ });
3172
+ const columnsConfig = pathColumnWidth === null ? void 0 : { [LIST_TABLE_PATH_COLUMN_INDEX]: {
3173
+ width: pathColumnWidth,
3174
+ truncate: pathColumnWidth
3175
+ } };
3076
3176
  const colorized = colorizeListTable({
3077
- rendered: table([[
3078
- "branch",
3079
- "dirty",
3080
- "merged",
3081
- "pr",
3082
- "locked",
3083
- "ahead",
3084
- "behind",
3085
- "path"
3086
- ], ...await Promise.all(snapshot.worktrees.map(async (worktree) => {
3087
- const distanceFromBase = await resolveAheadBehindAgainstBaseBranch({
3088
- repoRoot,
3089
- baseBranch: snapshot.baseBranch,
3090
- worktree
3091
- });
3092
- const isBaseBranch = worktree.branch !== null && snapshot.baseBranch !== null && worktree.branch === snapshot.baseBranch;
3093
- const mergedState = isBaseBranch === true ? "-" : worktree.merged.overall === true ? "merged" : worktree.merged.overall === false ? "unmerged" : "unknown";
3094
- const prState = formatPrDisplayState({
3095
- prStatus: worktree.pr.status,
3096
- isBaseBranch
3097
- });
3098
- return [
3099
- `${worktree.path === repoContext.currentWorktreeRoot ? "*" : " "} ${worktree.branch ?? "(detached)"}`,
3100
- worktree.dirty ? "dirty" : "clean",
3101
- mergedState,
3102
- prState,
3103
- worktree.locked.value ? "locked" : "-",
3104
- formatListUpstreamCount(distanceFromBase.ahead),
3105
- formatListUpstreamCount(distanceFromBase.behind),
3106
- formatDisplayPath(worktree.path)
3107
- ];
3108
- }))], {
3177
+ rendered: table(rows, {
3109
3178
  border: getBorderCharacters("norc"),
3110
3179
  drawHorizontalLine: (lineIndex, rowCount) => {
3111
3180
  return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount;
3112
- }
3181
+ },
3182
+ columns: columnsConfig
3113
3183
  }),
3114
3184
  theme
3115
3185
  });