@ogpoyraz/wtx 0.7.0 → 0.8.1

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/cli.mjs CHANGED
@@ -15331,6 +15331,11 @@ function verbose(message, isVerbose) {
15331
15331
  function indented(message) {
15332
15332
  console.log(` ${message}`);
15333
15333
  }
15334
+ function terminalLink(url2, text = url2, stream = process.stdout) {
15335
+ if (!stream.isTTY)
15336
+ return text;
15337
+ return `\x1B]8;;${url2}\x1B\\${text}\x1B]8;;\x1B\\`;
15338
+ }
15334
15339
  var c, quietMode = false;
15335
15340
  var init_log = __esm(() => {
15336
15341
  init_source();
@@ -23014,6 +23019,20 @@ async function getLatestCommit(repoPath, ref) {
23014
23019
  subject: stdout.substring(splitIdx + 1)
23015
23020
  };
23016
23021
  }
23022
+ async function resolveCommitSha(repoPath, ref, opts = { verbose: false, dryRun: false }) {
23023
+ try {
23024
+ const stdout = await gitExec(["-C", repoPath, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], opts);
23025
+ const sha = stdout.trim();
23026
+ if (!sha) {
23027
+ throw new Error("Git returned an empty commit id");
23028
+ }
23029
+ return sha;
23030
+ } catch (err) {
23031
+ const message = err instanceof Error ? err.message : String(err);
23032
+ throw new Error(`Ref '${ref}' does not resolve to a commit: ${message.split(`
23033
+ `)[0]}`);
23034
+ }
23035
+ }
23017
23036
  async function getDirtyFiles(worktreePath) {
23018
23037
  const stdout = await gitExec(["-C", worktreePath, "status", "--porcelain"]);
23019
23038
  return stdout.split(`
@@ -23131,19 +23150,195 @@ var init_git = __esm(() => {
23131
23150
  init_log();
23132
23151
  });
23133
23152
 
23134
- // src/lib/resolver.ts
23153
+ // src/lib/stack.ts
23135
23154
  import fs7 from "fs";
23136
23155
  import path15 from "path";
23156
+ function emptyMetadata() {
23157
+ return { version: 1, branches: {} };
23158
+ }
23159
+ function isRecord(value) {
23160
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23161
+ }
23162
+ function parseMetadata(value) {
23163
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.branches)) {
23164
+ throw new Error("invalid format or version");
23165
+ }
23166
+ const branches = {};
23167
+ for (const [branch, rawEntry] of Object.entries(value.branches)) {
23168
+ if (!isRecord(rawEntry))
23169
+ continue;
23170
+ if (typeof rawEntry.baseRef !== "string" || typeof rawEntry.baseSha !== "string")
23171
+ continue;
23172
+ branches[branch] = {
23173
+ baseRef: rawEntry.baseRef,
23174
+ baseSha: rawEntry.baseSha,
23175
+ explicit: rawEntry.explicit === true,
23176
+ createdAt: typeof rawEntry.createdAt === "string" ? rawEntry.createdAt : ""
23177
+ };
23178
+ }
23179
+ return { version: 1, branches };
23180
+ }
23181
+ async function metadataPath(repoPath, opts) {
23182
+ const commonDirOutput = await gitExec(["-C", repoPath, "rev-parse", "--git-common-dir"], { ...opts, dryRun: false });
23183
+ const commonDir = commonDirOutput.trim();
23184
+ if (!commonDir)
23185
+ return null;
23186
+ const resolvedCommonDir = path15.isAbsolute(commonDir) ? commonDir : path15.resolve(repoPath, commonDir);
23187
+ return path15.join(resolvedCommonDir, "wtx", "stack.json");
23188
+ }
23189
+ async function readStackMetadata(repoPath, opts = { verbose: false, dryRun: false }) {
23190
+ const filePath = await metadataPath(repoPath, opts);
23191
+ if (!filePath || !fs7.existsSync(filePath))
23192
+ return emptyMetadata();
23193
+ try {
23194
+ const raw = JSON.parse(fs7.readFileSync(filePath, "utf8"));
23195
+ return parseMetadata(raw);
23196
+ } catch (err) {
23197
+ const message = err instanceof Error ? err.message : String(err);
23198
+ throw new Error(`Failed to read stack metadata: ${message}`);
23199
+ }
23200
+ }
23201
+ async function writeStackMetadata(repoPath, metadata, opts) {
23202
+ if (opts.dryRun)
23203
+ return;
23204
+ const filePath = await metadataPath(repoPath, opts);
23205
+ if (!filePath)
23206
+ return;
23207
+ const dir = path15.dirname(filePath);
23208
+ fs7.mkdirSync(dir, { recursive: true });
23209
+ const tempPath = `${filePath}.${process.pid}.tmp`;
23210
+ fs7.writeFileSync(tempPath, `${JSON.stringify(metadata, null, 2)}
23211
+ `, "utf8");
23212
+ fs7.renameSync(tempPath, filePath);
23213
+ }
23214
+ async function recordStackEntry(repoPath, branch, entry, opts) {
23215
+ const metadata = await readStackMetadata(repoPath, opts);
23216
+ metadata.branches[branch] = entry;
23217
+ await writeStackMetadata(repoPath, metadata, opts);
23218
+ }
23219
+ async function removeStackEntry(repoPath, branch, opts) {
23220
+ const metadata = await readStackMetadata(repoPath, opts);
23221
+ if (!metadata.branches[branch])
23222
+ return;
23223
+ delete metadata.branches[branch];
23224
+ await writeStackMetadata(repoPath, metadata, opts);
23225
+ }
23226
+ async function renameStackEntry(repoPath, oldBranch, newBranch, opts) {
23227
+ const metadata = await readStackMetadata(repoPath, opts);
23228
+ const entry = metadata.branches[oldBranch];
23229
+ let changed = false;
23230
+ if (entry) {
23231
+ metadata.branches[newBranch] = entry;
23232
+ delete metadata.branches[oldBranch];
23233
+ changed = true;
23234
+ }
23235
+ for (const child of Object.values(metadata.branches)) {
23236
+ if (child.baseRef !== oldBranch && child.baseRef !== `refs/heads/${oldBranch}`)
23237
+ continue;
23238
+ child.baseRef = newBranch;
23239
+ changed = true;
23240
+ }
23241
+ if (changed)
23242
+ await writeStackMetadata(repoPath, metadata, opts);
23243
+ }
23244
+ function getStackChildren(metadata, branch) {
23245
+ return Object.entries(metadata.branches).filter(([, entry]) => entry.baseRef === branch || entry.baseRef === `refs/heads/${branch}`).map(([child]) => child).sort((a2, b) => a2.localeCompare(b));
23246
+ }
23247
+ function getStackAncestors(metadata, branch) {
23248
+ const ancestors = [branch];
23249
+ const seen = new Set([branch]);
23250
+ let current = branch;
23251
+ while (true) {
23252
+ const entry = metadata.branches[current];
23253
+ if (!entry || seen.has(entry.baseRef))
23254
+ break;
23255
+ ancestors.unshift(entry.baseRef);
23256
+ seen.add(entry.baseRef);
23257
+ current = entry.baseRef;
23258
+ }
23259
+ return ancestors;
23260
+ }
23261
+ function resolveParentBranch(baseRef, branches) {
23262
+ if (!baseRef)
23263
+ return null;
23264
+ if (branches.has(baseRef))
23265
+ return baseRef;
23266
+ const headRef = baseRef.replace(/^refs\/heads\//, "");
23267
+ if (branches.has(headRef))
23268
+ return headRef;
23269
+ const remoteRef = baseRef.replace(/^refs\/remotes\/[^/]+\//, "");
23270
+ if (branches.has(remoteRef))
23271
+ return remoteRef;
23272
+ const slash = baseRef.indexOf("/");
23273
+ if (slash > 0) {
23274
+ const shortRef = baseRef.substring(slash + 1);
23275
+ if (branches.has(shortRef))
23276
+ return shortRef;
23277
+ }
23278
+ return null;
23279
+ }
23280
+ function buildStackHierarchy(items, getBranch, getBase, compare) {
23281
+ const branchNames = new Set(items.map(getBranch).filter((branch) => branch !== undefined));
23282
+ const byBranch = new Map;
23283
+ const children = new Map;
23284
+ const roots = [];
23285
+ for (const item of items) {
23286
+ const branch = getBranch(item);
23287
+ if (branch)
23288
+ byBranch.set(branch, item);
23289
+ }
23290
+ for (const item of items) {
23291
+ const branch = getBranch(item);
23292
+ const parent = resolveParentBranch(getBase(item), branchNames);
23293
+ if (!parent || parent === branch || !byBranch.has(parent)) {
23294
+ roots.push(item);
23295
+ continue;
23296
+ }
23297
+ const siblings = children.get(parent) ?? [];
23298
+ siblings.push(item);
23299
+ children.set(parent, siblings);
23300
+ }
23301
+ const result = [];
23302
+ const visited = new Set;
23303
+ const visit = (item, depth, ancestorPrefix, isLast) => {
23304
+ const branch = getBranch(item);
23305
+ if (branch && visited.has(branch))
23306
+ return;
23307
+ if (branch)
23308
+ visited.add(branch);
23309
+ const prefix = depth === 0 ? "" : `${ancestorPrefix}${isLast ? "└─ " : "├─ "}`;
23310
+ result.push({ item, depth, prefix });
23311
+ const childRows = [...branch ? children.get(branch) ?? [] : []].sort(compare);
23312
+ const childPrefix = depth === 0 ? "" : `${ancestorPrefix}${isLast ? " " : "│ "}`;
23313
+ childRows.forEach((child, index) => {
23314
+ visit(child, depth + 1, childPrefix, index === childRows.length - 1);
23315
+ });
23316
+ };
23317
+ roots.sort(compare).forEach((root) => visit(root, 0, "", true));
23318
+ for (const item of items) {
23319
+ const branch = getBranch(item);
23320
+ if (!branch || !visited.has(branch))
23321
+ visit(item, 0, "", true);
23322
+ }
23323
+ return result;
23324
+ }
23325
+ var init_stack = __esm(() => {
23326
+ init_git();
23327
+ });
23328
+
23329
+ // src/lib/resolver.ts
23330
+ import fs8 from "fs";
23331
+ import path16 from "path";
23137
23332
  function detectRepoFromCwd(config2) {
23138
23333
  const cwd = process.cwd();
23139
23334
  const root = expandTilde(config2.root);
23140
23335
  for (const name of Object.keys(config2.repos)) {
23141
- const mainPath = path15.join(root, name);
23142
- const wtRoot = path15.join(root, `${name}${config2.postfix}`);
23143
- if (cwd === mainPath || cwd.startsWith(mainPath + path15.sep)) {
23336
+ const mainPath = path16.join(root, name);
23337
+ const wtRoot = path16.join(root, `${name}${config2.postfix}`);
23338
+ if (cwd === mainPath || cwd.startsWith(mainPath + path16.sep)) {
23144
23339
  return name;
23145
23340
  }
23146
- if (cwd === wtRoot || cwd.startsWith(wtRoot + path15.sep)) {
23341
+ if (cwd === wtRoot || cwd.startsWith(wtRoot + path16.sep)) {
23147
23342
  return name;
23148
23343
  }
23149
23344
  }
@@ -23173,11 +23368,11 @@ function resolveRepos(config2, repoFilter) {
23173
23368
  return targetRepos.map((name) => {
23174
23369
  const mainPath = `${expandTilde(config2.root)}/${name}`;
23175
23370
  const wtRoot = `${expandTilde(config2.root)}/${name}${config2.postfix}`;
23176
- if (!fs7.existsSync(mainPath)) {
23371
+ if (!fs8.existsSync(mainPath)) {
23177
23372
  throw new Error(`Repo directory not found at ${mainPath}`);
23178
23373
  }
23179
- const gitDir = path15.join(mainPath, ".git");
23180
- if (!fs7.existsSync(gitDir)) {
23374
+ const gitDir = path16.join(mainPath, ".git");
23375
+ if (!fs8.existsSync(gitDir)) {
23181
23376
  throw new Error(`Not a git repository: ${mainPath}`);
23182
23377
  }
23183
23378
  return {
@@ -23240,14 +23435,14 @@ function expandTemplate(template, vars) {
23240
23435
  }
23241
23436
 
23242
23437
  // src/lib/deps/workspaces.ts
23243
- import fs10 from "fs";
23244
- import path17 from "path";
23438
+ import fs11 from "fs";
23439
+ import path18 from "path";
23245
23440
  function getWorkspaceDirs(rootDir) {
23246
23441
  const dirs = new Set;
23247
- const pkgPath = path17.join(rootDir, "package.json");
23248
- if (fs10.existsSync(pkgPath)) {
23442
+ const pkgPath = path18.join(rootDir, "package.json");
23443
+ if (fs11.existsSync(pkgPath)) {
23249
23444
  try {
23250
- const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
23445
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
23251
23446
  if (pkg.workspaces) {
23252
23447
  let ws = [];
23253
23448
  if (Array.isArray(pkg.workspaces)) {
@@ -23261,10 +23456,10 @@ function getWorkspaceDirs(rootDir) {
23261
23456
  }
23262
23457
  } catch {}
23263
23458
  }
23264
- const pnpmPath = path17.join(rootDir, "pnpm-workspace.yaml");
23265
- if (fs10.existsSync(pnpmPath)) {
23459
+ const pnpmPath = path18.join(rootDir, "pnpm-workspace.yaml");
23460
+ if (fs11.existsSync(pnpmPath)) {
23266
23461
  try {
23267
- const content = fs10.readFileSync(pnpmPath, "utf-8");
23462
+ const content = fs11.readFileSync(pnpmPath, "utf-8");
23268
23463
  const lines = content.split(/\r?\n/);
23269
23464
  let inPackages = false;
23270
23465
  for (let line of lines) {
@@ -23295,8 +23490,8 @@ function resolvePattern(rootDir, pattern, out) {
23295
23490
  const normalized = pattern.replace(/\/+$/, "");
23296
23491
  const hasGlob = normalized.includes("*");
23297
23492
  if (!hasGlob) {
23298
- const fullDir = path17.join(rootDir, normalized);
23299
- if (fs10.existsSync(fullDir) && fs10.statSync(fullDir).isDirectory()) {
23493
+ const fullDir = path18.join(rootDir, normalized);
23494
+ if (fs11.existsSync(fullDir) && fs11.statSync(fullDir).isDirectory()) {
23300
23495
  out.add(normalized);
23301
23496
  }
23302
23497
  return;
@@ -23311,12 +23506,12 @@ function globToRegex(pattern) {
23311
23506
  function collectMatchingDirs(base2, rel, regex, out, depth) {
23312
23507
  if (depth > 8)
23313
23508
  return;
23314
- if (rel && regex.test(rel) && fs10.existsSync(path17.join(base2, rel, "package.json"))) {
23509
+ if (rel && regex.test(rel) && fs11.existsSync(path18.join(base2, rel, "package.json"))) {
23315
23510
  out.add(rel);
23316
23511
  }
23317
23512
  let entries;
23318
23513
  try {
23319
- entries = fs10.readdirSync(rel ? path17.join(base2, rel) : base2, { withFileTypes: true });
23514
+ entries = fs11.readdirSync(rel ? path18.join(base2, rel) : base2, { withFileTypes: true });
23320
23515
  } catch {
23321
23516
  return;
23322
23517
  }
@@ -23324,7 +23519,7 @@ function collectMatchingDirs(base2, rel, regex, out, depth) {
23324
23519
  if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) {
23325
23520
  continue;
23326
23521
  }
23327
- const nextRel = rel ? path17.posix.join(rel, entry.name) : entry.name;
23522
+ const nextRel = rel ? path18.posix.join(rel, entry.name) : entry.name;
23328
23523
  if (!regex.test(nextRel) && !couldMatchDeeper(regex.source, nextRel))
23329
23524
  continue;
23330
23525
  collectMatchingDirs(base2, nextRel, regex, out, depth + 1);
@@ -23336,21 +23531,21 @@ function couldMatchDeeper(regexSource, rel) {
23336
23531
  var init_workspaces = () => {};
23337
23532
 
23338
23533
  // src/lib/deps/diff.ts
23339
- import fs11 from "fs";
23340
- import path18 from "path";
23534
+ import fs12 from "fs";
23535
+ import path19 from "path";
23341
23536
  function filesMatch(wtPath, mainPath, fileNames) {
23342
23537
  for (const name of fileNames) {
23343
- const wtFile = path18.join(wtPath, name);
23344
- const mainFile = path18.join(mainPath, name);
23345
- const wtExists = fs11.existsSync(wtFile);
23346
- const mainExists = fs11.existsSync(mainFile);
23538
+ const wtFile = path19.join(wtPath, name);
23539
+ const mainFile = path19.join(mainPath, name);
23540
+ const wtExists = fs12.existsSync(wtFile);
23541
+ const mainExists = fs12.existsSync(mainFile);
23347
23542
  if (wtExists !== mainExists)
23348
23543
  return false;
23349
23544
  if (!wtExists)
23350
23545
  continue;
23351
23546
  try {
23352
- const wtContent = fs11.readFileSync(wtFile);
23353
- const mainContent = fs11.readFileSync(mainFile);
23547
+ const wtContent = fs12.readFileSync(wtFile);
23548
+ const mainContent = fs12.readFileSync(mainFile);
23354
23549
  if (!wtContent.equals(mainContent))
23355
23550
  return false;
23356
23551
  } catch {
@@ -23368,7 +23563,7 @@ function getWorkspaceDelta(wtPath, mainPath, lockfileNames) {
23368
23563
  const changedWorkspaces = [];
23369
23564
  for (const ws of allWorkspaces) {
23370
23565
  const wsFiles = ["package.json"];
23371
- if (!filesMatch(path18.join(wtPath, ws), path18.join(mainPath, ws), wsFiles)) {
23566
+ if (!filesMatch(path19.join(wtPath, ws), path19.join(mainPath, ws), wsFiles)) {
23372
23567
  changedWorkspaces.push(ws);
23373
23568
  }
23374
23569
  }
@@ -23379,8 +23574,8 @@ var init_diff = __esm(() => {
23379
23574
  });
23380
23575
 
23381
23576
  // src/lib/deps/adapters/bun.ts
23382
- import fs12 from "fs";
23383
- import path19 from "path";
23577
+ import fs13 from "fs";
23578
+ import path20 from "path";
23384
23579
  var bunAdapter;
23385
23580
  var init_bun = __esm(() => {
23386
23581
  init_diff();
@@ -23388,7 +23583,7 @@ var init_bun = __esm(() => {
23388
23583
  bunAdapter = {
23389
23584
  id: "bun",
23390
23585
  displayName: "bun",
23391
- detect: (dir) => fs12.existsSync(path19.join(dir, "bun.lockb")) || fs12.existsSync(path19.join(dir, "bun.lock")),
23586
+ detect: (dir) => fs13.existsSync(path20.join(dir, "bun.lockb")) || fs13.existsSync(path20.join(dir, "bun.lock")),
23392
23587
  lockfileNames: ["bun.lockb", "bun.lock"],
23393
23588
  definitionsMatch: (wtPath, mainPath) => {
23394
23589
  const delta = getWorkspaceDelta(wtPath, mainPath, ["bun.lockb", "bun.lock"]);
@@ -23416,8 +23611,8 @@ var init_bun = __esm(() => {
23416
23611
  });
23417
23612
 
23418
23613
  // src/lib/deps/adapters/pnpm.ts
23419
- import fs13 from "fs";
23420
- import path20 from "path";
23614
+ import fs14 from "fs";
23615
+ import path21 from "path";
23421
23616
  var pnpmAdapter;
23422
23617
  var init_pnpm = __esm(() => {
23423
23618
  init_diff();
@@ -23425,7 +23620,7 @@ var init_pnpm = __esm(() => {
23425
23620
  pnpmAdapter = {
23426
23621
  id: "pnpm",
23427
23622
  displayName: "pnpm",
23428
- detect: (dir) => fs13.existsSync(path20.join(dir, "pnpm-lock.yaml")),
23623
+ detect: (dir) => fs14.existsSync(path21.join(dir, "pnpm-lock.yaml")),
23429
23624
  lockfileNames: ["pnpm-lock.yaml"],
23430
23625
  definitionsMatch: (wtPath, mainPath) => {
23431
23626
  const delta = getWorkspaceDelta(wtPath, mainPath, ["pnpm-lock.yaml"]);
@@ -23453,8 +23648,8 @@ var init_pnpm = __esm(() => {
23453
23648
  });
23454
23649
 
23455
23650
  // src/lib/deps/adapters/yarn.ts
23456
- import fs14 from "fs";
23457
- import path21 from "path";
23651
+ import fs15 from "fs";
23652
+ import path22 from "path";
23458
23653
  var yarnAdapter;
23459
23654
  var init_yarn = __esm(() => {
23460
23655
  init_diff();
@@ -23462,7 +23657,7 @@ var init_yarn = __esm(() => {
23462
23657
  yarnAdapter = {
23463
23658
  id: "yarn",
23464
23659
  displayName: "yarn",
23465
- detect: (dir) => fs14.existsSync(path21.join(dir, "yarn.lock")),
23660
+ detect: (dir) => fs15.existsSync(path22.join(dir, "yarn.lock")),
23466
23661
  lockfileNames: ["yarn.lock"],
23467
23662
  definitionsMatch: (wtPath, mainPath) => {
23468
23663
  const delta = getWorkspaceDelta(wtPath, mainPath, ["yarn.lock"]);
@@ -23484,8 +23679,8 @@ var init_yarn = __esm(() => {
23484
23679
  });
23485
23680
 
23486
23681
  // src/lib/deps/adapters/npm.ts
23487
- import fs15 from "fs";
23488
- import path22 from "path";
23682
+ import fs16 from "fs";
23683
+ import path23 from "path";
23489
23684
  var npmAdapter;
23490
23685
  var init_npm = __esm(() => {
23491
23686
  init_diff();
@@ -23493,7 +23688,7 @@ var init_npm = __esm(() => {
23493
23688
  npmAdapter = {
23494
23689
  id: "npm",
23495
23690
  displayName: "npm",
23496
- detect: (dir) => fs15.existsSync(path22.join(dir, "package-lock.json")),
23691
+ detect: (dir) => fs16.existsSync(path23.join(dir, "package-lock.json")),
23497
23692
  lockfileNames: ["package-lock.json"],
23498
23693
  definitionsMatch: (wtPath, mainPath) => {
23499
23694
  const delta = getWorkspaceDelta(wtPath, mainPath, ["package-lock.json"]);
@@ -23850,47 +24045,47 @@ var init_registry = __esm(() => {
23850
24045
  });
23851
24046
 
23852
24047
  // src/lib/deps/linking.ts
23853
- import fs16 from "fs";
23854
- import path23 from "path";
24048
+ import fs17 from "fs";
24049
+ import path24 from "path";
23855
24050
  function performSafeLink(wtPath, mainPath, dryRun, quiet) {
23856
- const wtNm = path23.join(wtPath, "node_modules");
23857
- const mainNm = path23.join(mainPath, "node_modules");
23858
- if (!fs16.existsSync(mainNm)) {
24051
+ const wtNm = path24.join(wtPath, "node_modules");
24052
+ const mainNm = path24.join(mainPath, "node_modules");
24053
+ if (!fs17.existsSync(mainNm)) {
23859
24054
  return;
23860
24055
  }
23861
24056
  let existingLink = false;
23862
24057
  try {
23863
- const stat = fs16.lstatSync(wtNm);
24058
+ const stat = fs17.lstatSync(wtNm);
23864
24059
  existingLink = stat.isSymbolicLink();
23865
24060
  } catch {}
23866
24061
  if (existingLink) {
23867
24062
  if (!quiet)
23868
24063
  stepProgress("Removing whole-directory symlink to prepare for safe linking...");
23869
24064
  if (!dryRun) {
23870
- fs16.unlinkSync(wtNm);
24065
+ fs17.unlinkSync(wtNm);
23871
24066
  }
23872
24067
  }
23873
- if (!dryRun && !fs16.existsSync(wtNm)) {
23874
- fs16.mkdirSync(wtNm, { recursive: true });
24068
+ if (!dryRun && !fs17.existsSync(wtNm)) {
24069
+ fs17.mkdirSync(wtNm, { recursive: true });
23875
24070
  }
23876
24071
  const entriesToLink = [];
23877
24072
  try {
23878
- const mainEntries = fs16.readdirSync(mainNm, { withFileTypes: true });
24073
+ const mainEntries = fs17.readdirSync(mainNm, { withFileTypes: true });
23879
24074
  for (const entry of mainEntries) {
23880
24075
  if (entry.name === ".bin") {
23881
24076
  entriesToLink.push({
23882
24077
  name: ".bin",
23883
- target: path23.join(mainNm, ".bin"),
24078
+ target: path24.join(mainNm, ".bin"),
23884
24079
  isBin: true
23885
24080
  });
23886
24081
  } else if (entry.name.startsWith("@") && entry.isDirectory()) {
23887
- const scopePath = path23.join(mainNm, entry.name);
24082
+ const scopePath = path24.join(mainNm, entry.name);
23888
24083
  try {
23889
- const scopedEntries = fs16.readdirSync(scopePath, { withFileTypes: true });
24084
+ const scopedEntries = fs17.readdirSync(scopePath, { withFileTypes: true });
23890
24085
  for (const scopedEntry of scopedEntries) {
23891
24086
  entriesToLink.push({
23892
- name: path23.join(entry.name, scopedEntry.name),
23893
- target: path23.join(scopePath, scopedEntry.name),
24087
+ name: path24.join(entry.name, scopedEntry.name),
24088
+ target: path24.join(scopePath, scopedEntry.name),
23894
24089
  isBin: false
23895
24090
  });
23896
24091
  }
@@ -23898,7 +24093,7 @@ function performSafeLink(wtPath, mainPath, dryRun, quiet) {
23898
24093
  } else {
23899
24094
  entriesToLink.push({
23900
24095
  name: entry.name,
23901
- target: path23.join(mainNm, entry.name),
24096
+ target: path24.join(mainNm, entry.name),
23902
24097
  isBin: false
23903
24098
  });
23904
24099
  }
@@ -23908,7 +24103,7 @@ function performSafeLink(wtPath, mainPath, dryRun, quiet) {
23908
24103
  let failedCount = 0;
23909
24104
  const failedNames = [];
23910
24105
  for (const { name, target, isBin } of entriesToLink) {
23911
- const linkPath = path23.join(wtNm, name);
24106
+ const linkPath = path24.join(wtNm, name);
23912
24107
  if (dryRun) {
23913
24108
  if (!quiet)
23914
24109
  info(` [dry-run] Would link ${name}`);
@@ -23916,41 +24111,41 @@ function performSafeLink(wtPath, mainPath, dryRun, quiet) {
23916
24111
  continue;
23917
24112
  }
23918
24113
  try {
23919
- const parentDir = path23.dirname(linkPath);
23920
- if (!fs16.existsSync(parentDir)) {
23921
- fs16.mkdirSync(parentDir, { recursive: true });
24114
+ const parentDir = path24.dirname(linkPath);
24115
+ if (!fs17.existsSync(parentDir)) {
24116
+ fs17.mkdirSync(parentDir, { recursive: true });
23922
24117
  }
23923
24118
  let shouldLink = true;
23924
24119
  let backupPath = null;
23925
24120
  try {
23926
- const stat = fs16.lstatSync(linkPath);
24121
+ const stat = fs17.lstatSync(linkPath);
23927
24122
  if (stat.isSymbolicLink()) {
23928
- const existingTarget = fs16.readlinkSync(linkPath);
23929
- const resolvedExisting = path23.resolve(path23.dirname(linkPath), existingTarget);
24123
+ const existingTarget = fs17.readlinkSync(linkPath);
24124
+ const resolvedExisting = path24.resolve(path24.dirname(linkPath), existingTarget);
23930
24125
  if (resolvedExisting === target) {
23931
24126
  shouldLink = false;
23932
24127
  } else {
23933
- fs16.unlinkSync(linkPath);
24128
+ fs17.unlinkSync(linkPath);
23934
24129
  }
23935
24130
  } else {
23936
24131
  backupPath = `${linkPath}.wtx-old`;
23937
- fs16.renameSync(linkPath, backupPath);
24132
+ fs17.renameSync(linkPath, backupPath);
23938
24133
  }
23939
24134
  } catch {}
23940
24135
  if (shouldLink) {
23941
24136
  try {
23942
- const relTarget = path23.relative(path23.dirname(linkPath), target);
23943
- fs16.symlinkSync(relTarget, linkPath, isBin ? "dir" : fs16.statSync(target).isDirectory() ? "dir" : "file");
24137
+ const relTarget = path24.relative(path24.dirname(linkPath), target);
24138
+ fs17.symlinkSync(relTarget, linkPath, isBin ? "dir" : fs17.statSync(target).isDirectory() ? "dir" : "file");
23944
24139
  createdCount++;
23945
24140
  } catch (err) {
23946
24141
  if (backupPath) {
23947
- fs16.renameSync(backupPath, linkPath);
24142
+ fs17.renameSync(backupPath, linkPath);
23948
24143
  }
23949
24144
  throw err;
23950
24145
  }
23951
24146
  }
23952
- if (backupPath && fs16.existsSync(backupPath)) {
23953
- fs16.rmSync(backupPath, { recursive: true, force: true });
24147
+ if (backupPath && fs17.existsSync(backupPath)) {
24148
+ fs17.rmSync(backupPath, { recursive: true, force: true });
23954
24149
  }
23955
24150
  } catch {
23956
24151
  failedCount++;
@@ -23969,15 +24164,15 @@ var init_linking = __esm(() => {
23969
24164
  });
23970
24165
 
23971
24166
  // src/lib/deps/engine.ts
23972
- import fs17 from "fs";
23973
- import path24 from "path";
24167
+ import fs18 from "fs";
24168
+ import path25 from "path";
23974
24169
  function resolveAdapter2(dir, managerOverride) {
23975
24170
  return resolveAdapter(dir, managerOverride);
23976
24171
  }
23977
24172
  function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
23978
- const nmPath = path24.join(wtPath, "node_modules");
23979
- const mainNm = path24.join(mainPath, "node_modules");
23980
- if (!fs17.existsSync(mainNm)) {
24173
+ const nmPath = path25.join(wtPath, "node_modules");
24174
+ const mainNm = path25.join(mainPath, "node_modules");
24175
+ if (!fs18.existsSync(mainNm)) {
23981
24176
  if (!quiet)
23982
24177
  stepWarning("Main repo has no node_modules to symlink to");
23983
24178
  return;
@@ -23987,7 +24182,7 @@ function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
23987
24182
  let existingLink = false;
23988
24183
  let shouldRemove = false;
23989
24184
  try {
23990
- const stat = fs17.lstatSync(nmPath);
24185
+ const stat = fs18.lstatSync(nmPath);
23991
24186
  existingLink = stat.isSymbolicLink();
23992
24187
  shouldRemove = true;
23993
24188
  } catch {}
@@ -24001,9 +24196,9 @@ function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
24001
24196
  }
24002
24197
  if (!dryRun) {
24003
24198
  if (existingLink) {
24004
- fs17.unlinkSync(nmPath);
24199
+ fs18.unlinkSync(nmPath);
24005
24200
  } else {
24006
- fs17.rmSync(nmPath, { recursive: true, force: true });
24201
+ fs18.rmSync(nmPath, { recursive: true, force: true });
24007
24202
  }
24008
24203
  }
24009
24204
  }
@@ -24011,22 +24206,22 @@ function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
24011
24206
  if (!quiet)
24012
24207
  info(` [dry-run] Would symlink ${mainNm} to ${nmPath}`);
24013
24208
  } else {
24014
- fs17.symlinkSync(mainNm, nmPath);
24209
+ fs18.symlinkSync(mainNm, nmPath);
24015
24210
  }
24016
24211
  if (!quiet)
24017
24212
  stepSuccess("Symlinked node_modules", mainNm);
24018
24213
  }
24019
24214
  function detectCommonLinkageState(wtPath, mainPath) {
24020
- const nodeModulesPath = path24.join(wtPath, "node_modules");
24215
+ const nodeModulesPath = path25.join(wtPath, "node_modules");
24021
24216
  try {
24022
- const stat = fs17.lstatSync(nodeModulesPath);
24217
+ const stat = fs18.lstatSync(nodeModulesPath);
24023
24218
  if (stat.isSymbolicLink()) {
24024
- const target = fs17.readlinkSync(nodeModulesPath);
24025
- const resolvedTarget = path24.resolve(wtPath, target);
24219
+ const target = fs18.readlinkSync(nodeModulesPath);
24220
+ const resolvedTarget = path25.resolve(wtPath, target);
24026
24221
  const resolvedMain = safeResolve(mainPath);
24027
24222
  let targetExists = false;
24028
24223
  try {
24029
- fs17.statSync(resolvedTarget);
24224
+ fs18.statSync(resolvedTarget);
24030
24225
  targetExists = true;
24031
24226
  } catch {
24032
24227
  targetExists = false;
@@ -24034,14 +24229,14 @@ function detectCommonLinkageState(wtPath, mainPath) {
24034
24229
  if (!targetExists) {
24035
24230
  return { state: "broken", target };
24036
24231
  }
24037
- if (!isWithin(resolvedMain, resolvedTarget) && resolvedTarget !== path24.join(resolvedMain, "node_modules")) {
24232
+ if (!isWithin(resolvedMain, resolvedTarget) && resolvedTarget !== path25.join(resolvedMain, "node_modules")) {
24038
24233
  return { state: "external", target };
24039
24234
  }
24040
24235
  return { state: "linked-whole", target };
24041
24236
  } else if (stat.isDirectory()) {
24042
24237
  let isLinkedPackages = false;
24043
24238
  try {
24044
- const entries = fs17.readdirSync(nodeModulesPath, { withFileTypes: true });
24239
+ const entries = fs18.readdirSync(nodeModulesPath, { withFileTypes: true });
24045
24240
  for (const entry of entries) {
24046
24241
  if (entry.isSymbolicLink()) {
24047
24242
  isLinkedPackages = true;
@@ -24055,7 +24250,7 @@ function detectCommonLinkageState(wtPath, mainPath) {
24055
24250
  return { state: "installed" };
24056
24251
  }
24057
24252
  } catch {
24058
- if (!fs17.existsSync(nodeModulesPath)) {
24253
+ if (!fs18.existsSync(nodeModulesPath)) {
24059
24254
  return { state: "missing" };
24060
24255
  }
24061
24256
  }
@@ -24169,7 +24364,7 @@ function findRepoDepsContext(wtPath) {
24169
24364
  try {
24170
24365
  const config2 = loadConfig();
24171
24366
  const repos = resolveRepos(config2, []);
24172
- const repo = repos.find((r) => wtPath === r.wtRoot || wtPath.startsWith(r.wtRoot + "/"));
24367
+ const repo = repos.find((r) => wtPath === r.mainPath || wtPath === r.wtRoot || wtPath.startsWith(r.wtRoot + "/"));
24173
24368
  if (repo) {
24174
24369
  return {
24175
24370
  name: repo.name,
@@ -24250,18 +24445,18 @@ var init_deps = __esm(() => {
24250
24445
  });
24251
24446
 
24252
24447
  // src/lib/forge/map.ts
24253
- function isRecord(value) {
24448
+ function isRecord2(value) {
24254
24449
  return typeof value === "object" && value !== null && !Array.isArray(value);
24255
24450
  }
24256
24451
  function asCheckItem(value) {
24257
- if (!isRecord(value))
24452
+ if (!isRecord2(value))
24258
24453
  return null;
24259
24454
  return {
24260
24455
  __typename: typeof value.__typename === "string" ? value.__typename : undefined,
24261
24456
  status: typeof value.status === "string" ? value.status : null,
24262
24457
  conclusion: typeof value.conclusion === "string" ? value.conclusion : null,
24263
24458
  state: typeof value.state === "string" ? value.state : null,
24264
- commit: isRecord(value.commit) ? value.commit : null
24459
+ commit: isRecord2(value.commit) ? value.commit : null
24265
24460
  };
24266
24461
  }
24267
24462
  function collectCheckItems(raw) {
@@ -24335,7 +24530,7 @@ function mapReviewDecision(raw) {
24335
24530
  return null;
24336
24531
  }
24337
24532
  function mapGithubPr(raw) {
24338
- if (!isRecord(raw))
24533
+ if (!isRecord2(raw))
24339
24534
  return null;
24340
24535
  const number4 = typeof raw.number === "number" ? raw.number : null;
24341
24536
  const headRefName = typeof raw.headRefName === "string" ? raw.headRefName : null;
@@ -24346,9 +24541,10 @@ function mapGithubPr(raw) {
24346
24541
  const url2 = typeof raw.url === "string" ? raw.url : "";
24347
24542
  const isDraft = raw.isDraft === true;
24348
24543
  const updatedAt = typeof raw.updatedAt === "string" ? raw.updatedAt : "";
24544
+ const baseRefName = typeof raw.baseRefName === "string" ? raw.baseRefName : undefined;
24349
24545
  const checks3 = bucketChecks(collectCheckItems(raw.statusCheckRollup));
24350
24546
  const author = raw.author;
24351
- const authorLogin = isRecord(author) && typeof author.login === "string" ? author.login : null;
24547
+ const authorLogin = isRecord2(author) && typeof author.login === "string" ? author.login : null;
24352
24548
  return {
24353
24549
  number: number4,
24354
24550
  authorLogin,
@@ -24359,12 +24555,13 @@ function mapGithubPr(raw) {
24359
24555
  mergeable: mapMergeable(raw.mergeable),
24360
24556
  checks: checks3,
24361
24557
  reviewDecision: mapReviewDecision(raw.reviewDecision),
24558
+ ...baseRefName ? { baseRefName } : {},
24362
24559
  unresolvedThreads: 0,
24363
24560
  updatedAt
24364
24561
  };
24365
24562
  }
24366
24563
  function mapPrHead(raw) {
24367
- if (!isRecord(raw))
24564
+ if (!isRecord2(raw))
24368
24565
  return null;
24369
24566
  const number4 = typeof raw.number === "number" ? raw.number : null;
24370
24567
  const headRefName = typeof raw.headRefName === "string" ? raw.headRefName : null;
@@ -24375,12 +24572,13 @@ function mapPrHead(raw) {
24375
24572
  const url2 = typeof raw.url === "string" ? raw.url : "";
24376
24573
  const isDraft = raw.isDraft === true;
24377
24574
  const isCrossRepository = raw.isCrossRepository === true;
24575
+ const baseRefName = typeof raw.baseRefName === "string" ? raw.baseRefName : undefined;
24378
24576
  let headOwnerLogin = null;
24379
- if (isRecord(raw.headRepositoryOwner) && typeof raw.headRepositoryOwner.login === "string") {
24577
+ if (isRecord2(raw.headRepositoryOwner) && typeof raw.headRepositoryOwner.login === "string") {
24380
24578
  headOwnerLogin = raw.headRepositoryOwner.login;
24381
24579
  }
24382
24580
  let headRepoName = null;
24383
- if (isRecord(raw.headRepository) && typeof raw.headRepository.name === "string") {
24581
+ if (isRecord2(raw.headRepository) && typeof raw.headRepository.name === "string") {
24384
24582
  headRepoName = raw.headRepository.name;
24385
24583
  }
24386
24584
  return {
@@ -24390,6 +24588,7 @@ function mapPrHead(raw) {
24390
24588
  state,
24391
24589
  isDraft,
24392
24590
  headRefName,
24591
+ ...baseRefName ? { baseRefName } : {},
24393
24592
  isCrossRepository,
24394
24593
  headOwnerLogin,
24395
24594
  headRepoName
@@ -24485,7 +24684,7 @@ async function ghExec(args, opts = {}) {
24485
24684
  }
24486
24685
  function mapWithBranch(raw) {
24487
24686
  const pr = mapGithubPr(raw);
24488
- if (!pr || !isRecord(raw) || typeof raw.headRefName !== "string")
24687
+ if (!pr || !isRecord2(raw) || typeof raw.headRefName !== "string")
24489
24688
  return null;
24490
24689
  return { branch: raw.headRefName, pr };
24491
24690
  }
@@ -24530,12 +24729,12 @@ async function fetchBranchPrs(ctx) {
24530
24729
  return results.filter((entry) => entry !== null);
24531
24730
  }
24532
24731
  function countUnresolvedThreads(result) {
24533
- if (!isRecord(result))
24732
+ if (!isRecord2(result))
24534
24733
  return 0;
24535
24734
  const threads = result.reviewThreads;
24536
- if (!isRecord(threads) || !Array.isArray(threads.nodes))
24735
+ if (!isRecord2(threads) || !Array.isArray(threads.nodes))
24537
24736
  return 0;
24538
- return threads.nodes.filter((node) => isRecord(node) && node.isResolved === false).length;
24737
+ return threads.nodes.filter((node) => isRecord2(node) && node.isResolved === false).length;
24539
24738
  }
24540
24739
  function buildThreadQuery(numbers) {
24541
24740
  const aliases = numbers.map((n2, i2) => `pr${i2}: pullRequest(number: ${n2}) { reviewThreads(first: ${THREADS_PER_PR}) { nodes { isResolved } } }`).join(" ");
@@ -24556,10 +24755,10 @@ async function enrichUnresolvedThreads(slug, prs, verboseFlag) {
24556
24755
  `name=${slug.name}`
24557
24756
  ], { verbose: verboseFlag });
24558
24757
  const payload = parseJson(stdout);
24559
- if (!isRecord(payload) || !isRecord(payload.data))
24758
+ if (!isRecord2(payload) || !isRecord2(payload.data))
24560
24759
  return;
24561
24760
  const repoData = payload.data.repository;
24562
- if (!isRecord(repoData))
24761
+ if (!isRecord2(repoData))
24563
24762
  return;
24564
24763
  prs.forEach((pr, i2) => {
24565
24764
  pr.unresolvedThreads = countUnresolvedThreads(repoData[`pr${i2}`]);
@@ -24589,7 +24788,7 @@ function createGithubAdapter(slug) {
24589
24788
  "-R",
24590
24789
  `${slug.owner}/${slug.name}`,
24591
24790
  "--json",
24592
- "number,title,url,state,isDraft,headRefName,isCrossRepository,headRepositoryOwner,headRepository"
24791
+ "number,title,url,state,isDraft,headRefName,baseRefName,isCrossRepository,headRepositoryOwner,headRepository"
24593
24792
  ];
24594
24793
  const stdout = await ghExec(args, opts);
24595
24794
  const parsed = parseJson(stdout);
@@ -24629,6 +24828,7 @@ var init_github = __esm(() => {
24629
24828
  "statusCheckRollup",
24630
24829
  "reviewDecision",
24631
24830
  "headRefName",
24831
+ "baseRefName",
24632
24832
  "updatedAt"
24633
24833
  ].join(",");
24634
24834
  REMOTE_URL_PATTERNS = [
@@ -24639,8 +24839,8 @@ var init_github = __esm(() => {
24639
24839
  });
24640
24840
 
24641
24841
  // src/lib/forge/index.ts
24642
- import fs18 from "fs";
24643
- import path25 from "path";
24842
+ import fs19 from "fs";
24843
+ import path26 from "path";
24644
24844
  function parsePrLink(link) {
24645
24845
  for (const forge of FORGES) {
24646
24846
  const ref = forge.parsePrLink(link);
@@ -24653,10 +24853,10 @@ function descriptorFor(id) {
24653
24853
  return FORGES.find((f) => f.id === id) ?? null;
24654
24854
  }
24655
24855
  function readOriginUrl(mainPath) {
24656
- const gitConfigPath = path25.join(mainPath, ".git", "config");
24657
- if (!fs18.existsSync(gitConfigPath))
24856
+ const gitConfigPath = path26.join(mainPath, ".git", "config");
24857
+ if (!fs19.existsSync(gitConfigPath))
24658
24858
  return null;
24659
- const content = fs18.readFileSync(gitConfigPath, "utf-8");
24859
+ const content = fs19.readFileSync(gitConfigPath, "utf-8");
24660
24860
  const originSection = content.match(/\[remote "origin"\]([^[]*)/);
24661
24861
  if (!originSection)
24662
24862
  return null;
@@ -24706,7 +24906,7 @@ var init_forge = __esm(() => {
24706
24906
  });
24707
24907
 
24708
24908
  // src/lib/owner.ts
24709
- import fs19 from "fs";
24909
+ import fs20 from "fs";
24710
24910
  function deriveOwnership(input) {
24711
24911
  if (input.hasLocalChanges) {
24712
24912
  return { mine: true, author: null };
@@ -24733,7 +24933,7 @@ async function resolveOwnership(input) {
24733
24933
  }
24734
24934
  const { mainPath, branch, verbose: verbose2 } = input;
24735
24935
  let hasLocalChanges = false;
24736
- if (input.wtPath && fs19.existsSync(input.wtPath)) {
24936
+ if (input.wtPath && fs20.existsSync(input.wtPath)) {
24737
24937
  try {
24738
24938
  const dirtyFiles = await getDirtyFiles(input.wtPath);
24739
24939
  hasLocalChanges = dirtyFiles.length > 0;
@@ -24995,6 +25195,13 @@ async function fetchWorktreeData(opts, scope) {
24995
25195
  try {
24996
25196
  const mainBranch = await resolveMainBranch(repo, config2);
24997
25197
  const wts = await getWorktreeList(repo.mainPath);
25198
+ let stackMetadata = { version: 1, branches: {} };
25199
+ try {
25200
+ stackMetadata = await readStackMetadata(repo.mainPath, opts);
25201
+ } catch (err) {
25202
+ const message = err instanceof Error ? err.message : String(err);
25203
+ warnings.push({ repoName: repo.name, message });
25204
+ }
24998
25205
  const forge = resolveForge(repo);
24999
25206
  const branches = wts.map((w) => w.branch).filter(Boolean);
25000
25207
  let prMap = new Map;
@@ -25022,6 +25229,10 @@ async function fetchWorktreeData(opts, scope) {
25022
25229
  if (isMainCheckout && !branch) {
25023
25230
  branch = mainBranch;
25024
25231
  }
25232
+ const stackEntry = branch ? stackMetadata.branches[branch] : undefined;
25233
+ const pr = branch ? prMap.get(branch) : undefined;
25234
+ const base2 = stackEntry?.baseRef ?? pr?.baseRefName;
25235
+ const comparisonBase = stackEntry?.explicit ? stackEntry.baseRef : pr?.baseRefName && pr.baseRefName !== mainBranch ? pr.baseRefName : `origin/${mainBranch}`;
25025
25236
  const row = {
25026
25237
  repoName: repo.name,
25027
25238
  branch: branch || "(detached)",
@@ -25040,7 +25251,9 @@ async function fetchWorktreeData(opts, scope) {
25040
25251
  prUrl: null,
25041
25252
  owner: null,
25042
25253
  rebaseStatus: null,
25043
- depsStrategy: "none"
25254
+ depsStrategy: "none",
25255
+ base: base2,
25256
+ baseChanged: false
25044
25257
  };
25045
25258
  if (isMainCheckout) {
25046
25259
  allRows.push(row);
@@ -25052,7 +25265,7 @@ async function fetchWorktreeData(opts, scope) {
25052
25265
  })(),
25053
25266
  (async () => {
25054
25267
  try {
25055
- const stdout = await gitExec(["-C", wt.path, "rev-list", "--left-right", "--count", `origin/${mainBranch}...HEAD`], { dryRun: opts.dryRun });
25268
+ const stdout = await gitExec(["-C", wt.path, "rev-list", "--left-right", "--count", `${comparisonBase}...HEAD`], { dryRun: opts.dryRun });
25056
25269
  if (stdout) {
25057
25270
  const parts = stdout.trim().split(/\s+/);
25058
25271
  if (parts.length === 2) {
@@ -25062,6 +25275,16 @@ async function fetchWorktreeData(opts, scope) {
25062
25275
  }
25063
25276
  } catch {}
25064
25277
  })(),
25278
+ (async () => {
25279
+ if (!stackEntry?.explicit || opts.dryRun)
25280
+ return;
25281
+ try {
25282
+ const currentBaseSha = await resolveCommitSha(repo.mainPath, stackEntry.baseRef, opts);
25283
+ row.baseChanged = currentBaseSha !== stackEntry.baseSha;
25284
+ } catch {
25285
+ row.baseChanged = true;
25286
+ }
25287
+ })(),
25065
25288
  (async () => {
25066
25289
  row.rebaseStatus = detectInProgressRebase(wt.path);
25067
25290
  })(),
@@ -25073,7 +25296,6 @@ async function fetchWorktreeData(opts, scope) {
25073
25296
  })(),
25074
25297
  (async () => {
25075
25298
  if (branch && config2.user) {
25076
- const pr = prMap.get(branch);
25077
25299
  try {
25078
25300
  const owner = await resolveOwnership({
25079
25301
  configUser: config2.user,
@@ -25090,7 +25312,6 @@ async function fetchWorktreeData(opts, scope) {
25090
25312
  })()
25091
25313
  ]);
25092
25314
  if (branch) {
25093
- const pr = prMap.get(branch);
25094
25315
  if (pr) {
25095
25316
  row.prNumber = pr.number;
25096
25317
  row.prUrl = pr.url;
@@ -25127,6 +25348,7 @@ var init_data = __esm(() => {
25127
25348
  init_forge();
25128
25349
  init_owner();
25129
25350
  init_types2();
25351
+ init_stack();
25130
25352
  prCache = new Map;
25131
25353
  });
25132
25354
 
@@ -25147,14 +25369,16 @@ function matchesFilter(entry, term) {
25147
25369
  return true;
25148
25370
  if (entry.prUrl?.toLowerCase().includes(lower))
25149
25371
  return true;
25372
+ if (entry.base?.toLowerCase().includes(lower))
25373
+ return true;
25150
25374
  return false;
25151
25375
  }
25152
- function toggleSelection(current, path35) {
25376
+ function toggleSelection(current, path36) {
25153
25377
  const next = new Set(current);
25154
- if (next.has(path35)) {
25155
- next.delete(path35);
25378
+ if (next.has(path36)) {
25379
+ next.delete(path36);
25156
25380
  } else {
25157
- next.add(path35);
25381
+ next.add(path36);
25158
25382
  }
25159
25383
  return next;
25160
25384
  }
@@ -25181,9 +25405,38 @@ function rowSort(a2, b) {
25181
25405
  return 1;
25182
25406
  return a2.branch.localeCompare(b.branch);
25183
25407
  }
25408
+ function sortRowsHierarchically(rows) {
25409
+ return buildStackHierarchy(rows, (row) => row.branch, (row) => row.base, rowSort).map(({ item, depth, prefix }) => ({
25410
+ ...item,
25411
+ hierarchyDepth: depth,
25412
+ hierarchyPrefix: prefix
25413
+ }));
25414
+ }
25184
25415
  function sortBlocks(blocks) {
25185
25416
  return [...blocks].sort((a2, b) => a2.repoName.localeCompare(b.repoName));
25186
25417
  }
25418
+ function wrapText(text, width) {
25419
+ const lines = [];
25420
+ for (const paragraph of text.split(`
25421
+ `)) {
25422
+ let current = "";
25423
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
25424
+ if (!current) {
25425
+ current = word;
25426
+ } else if (current.length + 1 + word.length <= width) {
25427
+ current += ` ${word}`;
25428
+ } else {
25429
+ lines.push(current);
25430
+ current = word;
25431
+ }
25432
+ }
25433
+ lines.push(current);
25434
+ }
25435
+ return lines;
25436
+ }
25437
+ function isTapWithoutDrag(down, up) {
25438
+ return Math.abs(up.x - down.x) <= 1 && Math.abs(up.y - down.y) <= 1;
25439
+ }
25187
25440
  function mergeBlocks(prev, next, scope) {
25188
25441
  if (!scope)
25189
25442
  return sortBlocks(next);
@@ -25236,9 +25489,12 @@ function withCreatePlaceholders(blocks, creating) {
25236
25489
  if (!branches)
25237
25490
  return block;
25238
25491
  const placeholders = branches.map((br) => makePlaceholderRow(block.repoName, br));
25239
- return { ...block, rows: [...block.rows, ...placeholders].sort(rowSort) };
25492
+ return { ...block, rows: sortRowsHierarchically([...block.rows, ...placeholders]) };
25240
25493
  });
25241
25494
  }
25495
+ var init_utils = __esm(() => {
25496
+ init_stack();
25497
+ });
25242
25498
 
25243
25499
  // src/tui/hooks/useWorktrees.ts
25244
25500
  import { useState, useEffect, useCallback, useRef } from "react";
@@ -25278,8 +25534,7 @@ function useWorktrees(opts) {
25278
25534
  }
25279
25535
  const newBlocks = [];
25280
25536
  for (const [repoName, rows] of byRepo.entries()) {
25281
- rows.sort(rowSort);
25282
- newBlocks.push({ repoName, rows });
25537
+ newBlocks.push({ repoName, rows: sortRowsHierarchically(rows) });
25283
25538
  }
25284
25539
  newBlocks.sort((a2, b) => a2.repoName.localeCompare(b.repoName));
25285
25540
  const scopeSet = scope ? new Set(scope) : undefined;
@@ -25306,6 +25561,7 @@ function useWorktrees(opts) {
25306
25561
  var init_useWorktrees = __esm(() => {
25307
25562
  init_data();
25308
25563
  init_config();
25564
+ init_utils();
25309
25565
  });
25310
25566
 
25311
25567
  // src/tui/theme.ts
@@ -25339,9 +25595,91 @@ var init_theme = __esm(() => {
25339
25595
  };
25340
25596
  });
25341
25597
 
25598
+ // src/tui/hooks/use-tap.ts
25599
+ import { useRef as useRef2, useCallback as useCallback2 } from "react";
25600
+ function useTapHandler(onTap) {
25601
+ const down = useRef2(null);
25602
+ const onMouseDown = useCallback2((e) => {
25603
+ down.current = { x: e.x, y: e.y };
25604
+ }, []);
25605
+ const onMouseUp = useCallback2((e) => {
25606
+ const start = down.current;
25607
+ down.current = null;
25608
+ if (!start || !isTapWithoutDrag(start, e))
25609
+ return;
25610
+ onTap();
25611
+ }, [onTap]);
25612
+ return { onMouseDown, onMouseUp };
25613
+ }
25614
+ var init_use_tap = __esm(() => {
25615
+ init_utils();
25616
+ });
25617
+
25618
+ // src/tui/platform.ts
25619
+ function clipboardCandidatesFor(platform2, env2 = process.env) {
25620
+ if (platform2 === "darwin")
25621
+ return [{ cmd: "pbcopy", args: [] }];
25622
+ if (platform2 === "win32")
25623
+ return [{ cmd: "clip", args: [] }];
25624
+ const candidates = [];
25625
+ if (env2.WAYLAND_DISPLAY)
25626
+ candidates.push({ cmd: "wl-copy", args: [] });
25627
+ if (env2.DISPLAY) {
25628
+ candidates.push({ cmd: "xclip", args: ["-selection", "clipboard"] });
25629
+ candidates.push({ cmd: "xsel", args: ["--clipboard", "--input"] });
25630
+ }
25631
+ return candidates;
25632
+ }
25633
+ async function copyTextToClipboard(renderer, text) {
25634
+ if (!text)
25635
+ return false;
25636
+ const viaTerminal = renderer.copyToClipboardOSC52(text);
25637
+ let viaSystem = false;
25638
+ for (const candidate of clipboardCandidatesFor(process.platform)) {
25639
+ try {
25640
+ const proc = Bun.spawn([candidate.cmd, ...candidate.args], {
25641
+ stdin: "pipe",
25642
+ stdout: "ignore",
25643
+ stderr: "ignore"
25644
+ });
25645
+ proc.stdin.write(text);
25646
+ proc.stdin.end();
25647
+ if (await proc.exited === 0) {
25648
+ viaSystem = true;
25649
+ break;
25650
+ }
25651
+ } catch {}
25652
+ }
25653
+ return viaSystem || viaTerminal;
25654
+ }
25655
+ function browserCommandFor(platform2, url2) {
25656
+ if (!/^https?:\/\//.test(url2))
25657
+ return null;
25658
+ if (platform2 === "darwin")
25659
+ return { cmd: "open", args: [url2] };
25660
+ if (platform2 === "win32")
25661
+ return { cmd: "cmd", args: ["/c", "start", "", url2] };
25662
+ return { cmd: "xdg-open", args: [url2] };
25663
+ }
25664
+ async function openInBrowser(url2) {
25665
+ const command = browserCommandFor(process.platform, url2);
25666
+ if (!command)
25667
+ return false;
25668
+ try {
25669
+ const proc = Bun.spawn([command.cmd, ...command.args], {
25670
+ stdin: "ignore",
25671
+ stdout: "ignore",
25672
+ stderr: "ignore"
25673
+ });
25674
+ return await proc.exited === 0;
25675
+ } catch {
25676
+ return false;
25677
+ }
25678
+ }
25679
+
25342
25680
  // src/tui/components/WorktreeTable.tsx
25343
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
25344
- import { jsx, jsxs } from "@opentui/react/jsx-runtime";
25681
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
25682
+ import { jsx, jsxs, Fragment } from "@opentui/react/jsx-runtime";
25345
25683
  function statusBadge(row) {
25346
25684
  if (row.isMainCheckout)
25347
25685
  return { text: "[main]", fg: tokens.accent };
@@ -25356,22 +25694,25 @@ function statusBadge(row) {
25356
25694
  return { text: "clean", fg: tokens.dim };
25357
25695
  }
25358
25696
  function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }) {
25697
+ const prTap = useTapHandler(() => {
25698
+ if (row.prUrl)
25699
+ openInBrowser(row.prUrl);
25700
+ });
25359
25701
  const badge = indicator ? indicator.running ? { text: `${frame} ${indicator.verb}…`, fg: tokens.accent } : { text: `◌ ${indicator.verb}`, fg: tokens.dim } : row.isPendingCreate ? { text: `${frame} creating…`, fg: tokens.accent } : statusBadge(row);
25360
25702
  const disabled = indicator !== undefined || row.isPendingCreate === true;
25361
25703
  const primary = isSelected ? tokens.bright : disabled ? tokens.dim : tokens.fg;
25362
25704
  const divergence = row.ahead !== null && row.behind !== null && (row.ahead > 0 || row.behind > 0) ? ` · ↑${row.ahead} ↓${row.behind}` : "";
25363
- const prSegment = row.prNumber ? [
25364
- `· #${row.prNumber}`,
25365
- row.prState ?? "",
25366
- row.prChecks ? `(${row.prChecks})` : ""
25367
- ].filter(Boolean).join(" ") : "";
25368
25705
  const ownerSegment = row.owner ? ` · by ${row.owner}` : "";
25706
+ const baseSegment = row.base ? ` · base ${row.base}` : "";
25369
25707
  const rebaseSegment = !row.isMainCheckout && row.rebaseStatus && !row.isPrunable ? ` · ${row.rebaseStatus}` : "";
25708
+ const baseChangedSegment = row.baseChanged ? " · base moved" : "";
25709
+ const hierarchyPrefix = row.hierarchyPrefix ?? "";
25710
+ const secondaryIndent = " ".repeat(SECONDARY_INDENT.length + hierarchyPrefix.length);
25370
25711
  const secondary = [
25371
- `${SECONDARY_INDENT}${row.commitShort}`,
25712
+ `${secondaryIndent}${row.commitShort}`,
25372
25713
  divergence,
25373
- prSegment,
25374
- ownerSegment
25714
+ ownerSegment,
25715
+ baseSegment
25375
25716
  ].filter(Boolean).join(" ").trimEnd();
25376
25717
  return /* @__PURE__ */ jsxs("box", {
25377
25718
  id,
@@ -25389,6 +25730,10 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
25389
25730
  fg: tokens.accent,
25390
25731
  children: isMultiSelected ? "✓ " : " "
25391
25732
  }),
25733
+ /* @__PURE__ */ jsx("span", {
25734
+ fg: tokens.dim,
25735
+ children: hierarchyPrefix
25736
+ }),
25392
25737
  /* @__PURE__ */ jsx("span", {
25393
25738
  fg: primary,
25394
25739
  children: truncateBranch(row.branch)
@@ -25400,11 +25745,40 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
25400
25745
  ]
25401
25746
  }),
25402
25747
  /* @__PURE__ */ jsxs("text", {
25748
+ ...row.prUrl ? prTap : {},
25403
25749
  children: [
25404
25750
  /* @__PURE__ */ jsx("span", {
25405
25751
  fg: tokens.dim,
25406
25752
  children: secondary
25407
25753
  }),
25754
+ row.prNumber !== null && /* @__PURE__ */ jsxs(Fragment, {
25755
+ children: [
25756
+ /* @__PURE__ */ jsx("span", {
25757
+ fg: tokens.dim,
25758
+ children: secondary ? " · " : ""
25759
+ }),
25760
+ /* @__PURE__ */ jsx("span", {
25761
+ fg: tokens.accent,
25762
+ children: `#${row.prNumber}`
25763
+ }),
25764
+ row.prState && /* @__PURE__ */ jsx("span", {
25765
+ fg: tokens.dim,
25766
+ children: ` ${row.prState}`
25767
+ }),
25768
+ row.prChecks && /* @__PURE__ */ jsx("span", {
25769
+ fg: tokens.dim,
25770
+ children: ` (${row.prChecks})`
25771
+ }),
25772
+ row.prUrl && /* @__PURE__ */ jsx("span", {
25773
+ fg: tokens.dim,
25774
+ children: " ↗"
25775
+ })
25776
+ ]
25777
+ }),
25778
+ baseChangedSegment && /* @__PURE__ */ jsx("span", {
25779
+ fg: tokens.warning,
25780
+ children: baseChangedSegment
25781
+ }),
25408
25782
  rebaseSegment && /* @__PURE__ */ jsx("span", {
25409
25783
  fg: tokens.error,
25410
25784
  children: rebaseSegment
@@ -25415,7 +25789,7 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
25415
25789
  });
25416
25790
  }
25417
25791
  function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repoVerbs, rowVerbs }) {
25418
- const scrollRef = useRef2(null);
25792
+ const scrollRef = useRef3(null);
25419
25793
  useEffect2(() => {
25420
25794
  if (scrollRef.current?.scrollChildIntoView) {
25421
25795
  scrollRef.current.scrollChildIntoView("selected-row");
@@ -25485,11 +25859,16 @@ function WorktreeTable({ blocks, selectedIndex, selection = new Set, frame, repo
25485
25859
  var SECONDARY_INDENT = " ";
25486
25860
  var init_WorktreeTable = __esm(() => {
25487
25861
  init_theme();
25862
+ init_use_tap();
25488
25863
  });
25489
25864
 
25490
25865
  // src/tui/components/DetailPane.tsx
25491
- import { jsx as jsx2, jsxs as jsxs2, Fragment } from "@opentui/react/jsx-runtime";
25866
+ import { jsx as jsx2, jsxs as jsxs2, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
25492
25867
  function DetailPane({ selectedRow }) {
25868
+ const prTap = useTapHandler(() => {
25869
+ if (selectedRow?.prUrl)
25870
+ openInBrowser(selectedRow.prUrl);
25871
+ });
25493
25872
  if (!selectedRow) {
25494
25873
  return /* @__PURE__ */ jsx2("box", {
25495
25874
  id: "detail-pane",
@@ -25510,7 +25889,7 @@ function DetailPane({ selectedRow }) {
25510
25889
  const {
25511
25890
  repoName,
25512
25891
  branch,
25513
- path: path35,
25892
+ path: path36,
25514
25893
  commitShort,
25515
25894
  isMainCheckout,
25516
25895
  isLocked,
@@ -25524,7 +25903,9 @@ function DetailPane({ selectedRow }) {
25524
25903
  prUrl,
25525
25904
  owner,
25526
25905
  rebaseStatus,
25527
- depsStrategy
25906
+ depsStrategy,
25907
+ base: base2,
25908
+ baseChanged
25528
25909
  } = selectedRow;
25529
25910
  const aheadBehindStr = ahead !== null && behind !== null ? `${ahead} ahead, ${behind} behind` : "unknown";
25530
25911
  return /* @__PURE__ */ jsxs2("scrollbox", {
@@ -25579,7 +25960,7 @@ function DetailPane({ selectedRow }) {
25579
25960
  fg: tokens.fg,
25580
25961
  children: [
25581
25962
  " ",
25582
- path35
25963
+ path36
25583
25964
  ]
25584
25965
  })
25585
25966
  ]
@@ -25629,12 +26010,39 @@ function DetailPane({ selectedRow }) {
25629
26010
  })
25630
26011
  ]
25631
26012
  }),
26013
+ base2 && /* @__PURE__ */ jsxs2("text", {
26014
+ children: [
26015
+ /* @__PURE__ */ jsx2("span", {
26016
+ fg: tokens.dim,
26017
+ children: "Base:"
26018
+ }),
26019
+ /* @__PURE__ */ jsxs2("span", {
26020
+ fg: tokens.accent,
26021
+ children: [
26022
+ " ",
26023
+ base2
26024
+ ]
26025
+ })
26026
+ ]
26027
+ }),
26028
+ baseChanged && /* @__PURE__ */ jsxs2("text", {
26029
+ children: [
26030
+ /* @__PURE__ */ jsx2("span", {
26031
+ fg: tokens.dim,
26032
+ children: "Base state:"
26033
+ }),
26034
+ /* @__PURE__ */ jsx2("span", {
26035
+ fg: tokens.warning,
26036
+ children: " moved since recorded"
26037
+ })
26038
+ ]
26039
+ }),
25632
26040
  /* @__PURE__ */ jsxs2("text", {
25633
26041
  style: { marginTop: 1 },
25634
26042
  children: [
25635
26043
  /* @__PURE__ */ jsx2("span", {
25636
26044
  fg: tokens.dim,
25637
- children: "vs main:"
26045
+ children: base2 ? "vs base:" : "vs main:"
25638
26046
  }),
25639
26047
  /* @__PURE__ */ jsxs2("span", {
25640
26048
  fg: tokens.fg,
@@ -25675,7 +26083,7 @@ function DetailPane({ selectedRow }) {
25675
26083
  })
25676
26084
  ]
25677
26085
  }),
25678
- prNumber !== null && /* @__PURE__ */ jsxs2(Fragment, {
26086
+ prNumber !== null && /* @__PURE__ */ jsxs2(Fragment2, {
25679
26087
  children: [
25680
26088
  /* @__PURE__ */ jsxs2("text", {
25681
26089
  style: { marginTop: 1 },
@@ -25712,20 +26120,25 @@ function DetailPane({ selectedRow }) {
25712
26120
  })
25713
26121
  ]
25714
26122
  }),
25715
- prUrl && /* @__PURE__ */ jsxs2("text", {
25716
- children: [
25717
- /* @__PURE__ */ jsx2("span", {
25718
- fg: tokens.dim,
25719
- children: "URL:"
25720
- }),
25721
- /* @__PURE__ */ jsxs2("span", {
25722
- fg: tokens.fg,
25723
- children: [
25724
- " ",
25725
- prUrl
25726
- ]
25727
- })
25728
- ]
26123
+ prUrl && /* @__PURE__ */ jsx2("box", {
26124
+ ...prTap,
26125
+ children: /* @__PURE__ */ jsxs2("text", {
26126
+ selectable: false,
26127
+ children: [
26128
+ /* @__PURE__ */ jsx2("span", {
26129
+ fg: tokens.dim,
26130
+ children: "URL:"
26131
+ }),
26132
+ /* @__PURE__ */ jsx2("span", {
26133
+ fg: tokens.accent,
26134
+ children: ` ${prUrl}`
26135
+ }),
26136
+ /* @__PURE__ */ jsx2("span", {
26137
+ fg: tokens.dim,
26138
+ children: " ↗ click"
26139
+ })
26140
+ ]
26141
+ })
25729
26142
  })
25730
26143
  ]
25731
26144
  }),
@@ -25766,7 +26179,7 @@ function DetailPane({ selectedRow }) {
25766
26179
  fg: tokens.dim,
25767
26180
  children: "Actions:"
25768
26181
  }),
25769
- !isMainCheckout && /* @__PURE__ */ jsxs2(Fragment, {
26182
+ !isMainCheckout && /* @__PURE__ */ jsxs2(Fragment2, {
25770
26183
  children: [
25771
26184
  /* @__PURE__ */ jsxs2("text", {
25772
26185
  fg: tokens.dim,
@@ -25797,6 +26210,7 @@ function DetailPane({ selectedRow }) {
25797
26210
  }
25798
26211
  var init_DetailPane = __esm(() => {
25799
26212
  init_theme();
26213
+ init_use_tap();
25800
26214
  });
25801
26215
 
25802
26216
  // src/tui/components/Footer.tsx
@@ -25843,11 +26257,15 @@ function Footer({ loading, lastRefreshed, errorCount, message, busyText, spinner
25843
26257
  ]
25844
26258
  }) : null,
25845
26259
  errorCount > 0 ? /* @__PURE__ */ jsxs3("text", {
25846
- fg: tokens.error,
25847
26260
  children: [
25848
- errorCount,
25849
- " error",
25850
- errorCount !== 1 ? "s" : ""
26261
+ /* @__PURE__ */ jsx3("span", {
26262
+ fg: tokens.error,
26263
+ children: `${errorCount} error${errorCount !== 1 ? "s" : ""}`
26264
+ }),
26265
+ /* @__PURE__ */ jsx3("span", {
26266
+ fg: tokens.dim,
26267
+ children: " · e view"
26268
+ })
25851
26269
  ]
25852
26270
  }) : null,
25853
26271
  /* @__PURE__ */ jsx3("text", {
@@ -25960,15 +26378,18 @@ var init_HelpOverlay = __esm(() => {
25960
26378
  ["n", "Create new worktree (pick dependency strategy)"],
25961
26379
  ["f", "Fetch main for selected repo(s)"],
25962
26380
  ["p", "Pull latest changes for selected branch(es)"],
25963
- ["b", "Rebase selected onto main"],
26381
+ ["b", "Rebase selected onto base (or main)"],
25964
26382
  ["s", "Sync selected (env files + hooks)"],
25965
- ["i", "Install dependencies in selected worktree(s)"],
26383
+ ["i", "Install dependencies in selection (worktrees and main)"],
25966
26384
  ["m", "Rename selected worktree (branch + directory)"],
25967
26385
  ["o", "Open selected in IDE"],
25968
26386
  ["a", "Spawn agent in selected"],
25969
26387
  ["d", "Remove selected worktree(s)"],
26388
+ ["e", "View data warnings (when count > 0)"],
25970
26389
  ["r", "Refresh data"],
25971
26390
  ["H", "Action history"],
26391
+ ["cmd+c / ctrl+shift+c", "Copy selected text to clipboard"],
26392
+ ["click PR #/URL", "Open pull request in browser"],
25972
26393
  ["?", "Toggle help"],
25973
26394
  ["q/esc", "Quit"]
25974
26395
  ];
@@ -26136,38 +26557,38 @@ var init_ActionLogModal = __esm(() => {
26136
26557
  });
26137
26558
 
26138
26559
  // src/lib/history.ts
26139
- import fs30 from "fs";
26140
- import path35 from "path";
26560
+ import fs31 from "fs";
26561
+ import path36 from "path";
26141
26562
  import os4 from "os";
26142
26563
  function getHistoryDir() {
26143
- const stateHome = process.env.XDG_STATE_HOME ?? path35.join(os4.homedir(), ".local", "state");
26144
- return path35.join(stateHome, "wtx");
26564
+ const stateHome = process.env.XDG_STATE_HOME ?? path36.join(os4.homedir(), ".local", "state");
26565
+ return path36.join(stateHome, "wtx");
26145
26566
  }
26146
26567
  function getHistoryPath() {
26147
- return path35.join(getHistoryDir(), "history.jsonl");
26568
+ return path36.join(getHistoryDir(), "history.jsonl");
26148
26569
  }
26149
26570
  function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_KEEP_LINES) {
26150
26571
  const historyPath = getHistoryPath();
26151
26572
  let size = 0;
26152
26573
  try {
26153
- size = fs30.statSync(historyPath).size;
26574
+ size = fs31.statSync(historyPath).size;
26154
26575
  } catch {
26155
26576
  return;
26156
26577
  }
26157
26578
  if (size <= maxBytes)
26158
26579
  return;
26159
- const lines = fs30.readFileSync(historyPath, "utf-8").split(`
26580
+ const lines = fs31.readFileSync(historyPath, "utf-8").split(`
26160
26581
  `).filter(Boolean);
26161
26582
  const kept = lines.slice(-keepLines);
26162
26583
  const tmpPath = `${historyPath}.tmp.${process.pid}`;
26163
26584
  try {
26164
- fs30.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
26585
+ fs31.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
26165
26586
  `)}
26166
26587
  ` : "", "utf-8");
26167
- fs30.renameSync(tmpPath, historyPath);
26588
+ fs31.renameSync(tmpPath, historyPath);
26168
26589
  } catch (err) {
26169
- if (fs30.existsSync(tmpPath)) {
26170
- fs30.unlinkSync(tmpPath);
26590
+ if (fs31.existsSync(tmpPath)) {
26591
+ fs31.unlinkSync(tmpPath);
26171
26592
  }
26172
26593
  throw err;
26173
26594
  }
@@ -26175,18 +26596,18 @@ function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_
26175
26596
  function appendHistory(entry) {
26176
26597
  try {
26177
26598
  const dir = getHistoryDir();
26178
- if (!fs30.existsSync(dir)) {
26179
- fs30.mkdirSync(dir, { recursive: true });
26599
+ if (!fs31.existsSync(dir)) {
26600
+ fs31.mkdirSync(dir, { recursive: true });
26180
26601
  }
26181
26602
  rotateHistory();
26182
- fs30.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
26603
+ fs31.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
26183
26604
  `, "utf-8");
26184
26605
  } catch {}
26185
26606
  }
26186
26607
  function readRecentHistory(limit = 50) {
26187
26608
  let content;
26188
26609
  try {
26189
- content = fs30.readFileSync(getHistoryPath(), "utf-8");
26610
+ content = fs31.readFileSync(getHistoryPath(), "utf-8");
26190
26611
  } catch {
26191
26612
  return [];
26192
26613
  }
@@ -26424,7 +26845,7 @@ var init_ChoiceModal = __esm(() => {
26424
26845
  // src/tui/components/ConfigOverlay.tsx
26425
26846
  import { useState as useState5, useEffect as useEffect4 } from "react";
26426
26847
  import { useKeyboard as useKeyboard2 } from "@opentui/react";
26427
- import { jsx as jsx11, jsxs as jsxs10, Fragment as Fragment2 } from "@opentui/react/jsx-runtime";
26848
+ import { jsx as jsx11, jsxs as jsxs10, Fragment as Fragment3 } from "@opentui/react/jsx-runtime";
26428
26849
  function ConfigOverlay({ onClose, onSaved, onError }) {
26429
26850
  const [config2, setConfig] = useState5(null);
26430
26851
  const [viewState, setViewState] = useState5({ type: "main" });
@@ -26687,7 +27108,7 @@ function ConfigOverlay({ onClose, onSaved, onError }) {
26687
27108
  hint = "Remove this repository from config";
26688
27109
  }
26689
27110
  }
26690
- return /* @__PURE__ */ jsxs10(Fragment2, {
27111
+ return /* @__PURE__ */ jsxs10(Fragment3, {
26691
27112
  children: [
26692
27113
  /* @__PURE__ */ jsx11(Overlay, {
26693
27114
  title: "Configuration",
@@ -26787,12 +27208,54 @@ function ConfigOverlay({ onClose, onSaved, onError }) {
26787
27208
  }
26788
27209
  var init_ConfigOverlay = __esm(() => {
26789
27210
  init_Overlay();
27211
+ init_utils();
26790
27212
  init_theme();
26791
27213
  init_config();
26792
27214
  init_InputModal();
26793
27215
  init_ConfirmModal();
26794
27216
  });
26795
27217
 
27218
+ // src/tui/components/WarningsOverlay.tsx
27219
+ import { jsx as jsx12, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
27220
+ function WarningsOverlay({ warnings }) {
27221
+ return /* @__PURE__ */ jsxs11(Overlay, {
27222
+ title: `Warnings (${warnings.length})`,
27223
+ borderColor: tokens.warning,
27224
+ children: [
27225
+ /* @__PURE__ */ jsx12("box", {
27226
+ flexDirection: "column",
27227
+ style: { maxHeight: 30 },
27228
+ children: /* @__PURE__ */ jsx12("scrollbox", {
27229
+ flexGrow: 1,
27230
+ children: warnings.map((warning, i2) => /* @__PURE__ */ jsxs11("box", {
27231
+ flexDirection: "column",
27232
+ style: { marginBottom: 1 },
27233
+ children: [
27234
+ /* @__PURE__ */ jsx12("text", {
27235
+ fg: tokens.warning,
27236
+ children: `⚠ ${warning.repoName}`
27237
+ }),
27238
+ wrapText(warning.message, 56).map((line, j) => /* @__PURE__ */ jsx12("text", {
27239
+ fg: tokens.fg,
27240
+ children: ` ${line}`
27241
+ }, j))
27242
+ ]
27243
+ }, i2))
27244
+ })
27245
+ }),
27246
+ /* @__PURE__ */ jsx12("text", {
27247
+ style: { marginTop: 1, fg: tokens.dim },
27248
+ children: "Press any key to close"
27249
+ })
27250
+ ]
27251
+ });
27252
+ }
27253
+ var init_WarningsOverlay = __esm(() => {
27254
+ init_Overlay();
27255
+ init_theme();
27256
+ init_utils();
27257
+ });
27258
+
26796
27259
  // src/tui/hooks/useSpinnerFrame.ts
26797
27260
  import { useEffect as useEffect5, useState as useState6 } from "react";
26798
27261
  function useSpinnerFrame(active) {
@@ -26811,9 +27274,9 @@ var init_useSpinnerFrame = __esm(() => {
26811
27274
  });
26812
27275
 
26813
27276
  // src/tui/components/App.tsx
26814
- import { useState as useState7, useEffect as useEffect6, useMemo, useRef as useRef3, useCallback as useCallback2 } from "react";
27277
+ import { useState as useState7, useEffect as useEffect6, useMemo, useRef as useRef4, useCallback as useCallback3 } from "react";
26815
27278
  import { useKeyboard as useKeyboard3, useRenderer } from "@opentui/react";
26816
- import { jsx as jsx12, jsxs as jsxs11 } from "@opentui/react/jsx-runtime";
27279
+ import { jsx as jsx13, jsxs as jsxs12 } from "@opentui/react/jsx-runtime";
26817
27280
  function getWorktreePathFor(repoName, branch) {
26818
27281
  try {
26819
27282
  const config2 = loadConfig();
@@ -26829,12 +27292,14 @@ function App({ opts }) {
26829
27292
  const [selectedIndex, setSelectedIndex] = useState7(0);
26830
27293
  const [modal, setModal] = useState7({ type: "none" });
26831
27294
  const [actionMessage, setActionMessage] = useState7();
26832
- const messageTimer = useRef3(null);
27295
+ const messageTimer = useRef4(null);
26833
27296
  const [ops, setOps] = useState7([]);
26834
27297
  const [failedLogs, setFailedLogs] = useState7([]);
26835
- const nextOpId = useRef3(1);
27298
+ const nextOpId = useRef4(1);
26836
27299
  const [createModal, setCreateModal] = useState7(false);
26837
27300
  const [createError, setCreateError] = useState7();
27301
+ const [createBaseModal, setCreateBaseModal] = useState7(null);
27302
+ const [createBaseError, setCreateBaseError] = useState7();
26838
27303
  const [createDepsChoice, setCreateDepsChoice] = useState7(null);
26839
27304
  const [renameModal, setRenameModal] = useState7(false);
26840
27305
  const [renameError, setRenameError] = useState7();
@@ -26843,7 +27308,7 @@ function App({ opts }) {
26843
27308
  const [filterText, setFilterText] = useState7("");
26844
27309
  const [isFiltering, setIsFiltering] = useState7(false);
26845
27310
  const [selection, setSelection] = useState7(new Set);
26846
- const doRefresh = useCallback2(async (scope) => {
27311
+ const doRefresh = useCallback3(async (scope) => {
26847
27312
  const targets = scope ?? [
26848
27313
  ...new Set([...blocks.map((b) => b.repoName), ...pendingRepos])
26849
27314
  ];
@@ -26859,7 +27324,7 @@ function App({ opts }) {
26859
27324
  setModal({ type: "error", message: error52 });
26860
27325
  }
26861
27326
  }, [error52]);
26862
- const flash = useCallback2((message, ms = 3000) => {
27327
+ const flash = useCallback3((message, ms = 3000) => {
26863
27328
  if (messageTimer.current)
26864
27329
  clearTimeout(messageTimer.current);
26865
27330
  setActionMessage(message);
@@ -26990,7 +27455,7 @@ function App({ opts }) {
26990
27455
  setOps((prev) => [...prev, op]);
26991
27456
  executeOp(op, ["fetch", "--repo", repoNames.join(",")], repoNames).then(() => setSelection(new Set));
26992
27457
  };
26993
- const startCreate = (branch, repoName, deps) => {
27458
+ const startCreate = (branch, repoName, deps, base2) => {
26994
27459
  const op = {
26995
27460
  id: nextOpId.current++,
26996
27461
  kind: "create",
@@ -27003,6 +27468,8 @@ function App({ opts }) {
27003
27468
  };
27004
27469
  setOps((prev) => [...prev, op]);
27005
27470
  const args = ["create", branch, "--repo", repoName];
27471
+ if (base2)
27472
+ args.push("--base", base2);
27006
27473
  if (deps && deps !== "auto")
27007
27474
  args.push("--deps", deps);
27008
27475
  executeOp(op, args, [repoName]);
@@ -27031,6 +27498,13 @@ function App({ opts }) {
27031
27498
  }
27032
27499
  return;
27033
27500
  }
27501
+ if (createBaseModal) {
27502
+ if (key.name === "escape") {
27503
+ setCreateBaseModal(null);
27504
+ setCreateBaseError(undefined);
27505
+ }
27506
+ return;
27507
+ }
27034
27508
  if (createDepsChoice)
27035
27509
  return;
27036
27510
  if (renameModal) {
@@ -27041,7 +27515,7 @@ function App({ opts }) {
27041
27515
  return;
27042
27516
  }
27043
27517
  if (modal.type !== "none") {
27044
- if (modal.type === "error" || modal.type === "help" || modal.type === "history") {
27518
+ if (modal.type === "error" || modal.type === "help" || modal.type === "history" || modal.type === "warnings") {
27045
27519
  setModal({ type: "none" });
27046
27520
  if (modal.type === "error" && error52) {
27047
27521
  renderer.destroy();
@@ -27089,6 +27563,15 @@ function App({ opts }) {
27089
27563
  return;
27090
27564
  }
27091
27565
  }
27566
+ if ((key.super || key.meta || key.ctrl && key.shift) && key.name === "c") {
27567
+ const text = renderer.getSelection()?.getSelectedText() ?? "";
27568
+ if (!text) {
27569
+ flash("Nothing selected to copy");
27570
+ return;
27571
+ }
27572
+ copyTextToClipboard(renderer, text).then((ok) => flash(ok ? `Copied ${text.length} character${text.length !== 1 ? "s" : ""}` : "Copy failed"));
27573
+ return;
27574
+ }
27092
27575
  if (key.name === "q" || key.name === "escape" || key.name === "c" && key.ctrl) {
27093
27576
  if (key.name === "escape" && selection.size > 0) {
27094
27577
  setSelection(new Set);
@@ -27131,6 +27614,10 @@ function App({ opts }) {
27131
27614
  }
27132
27615
  return;
27133
27616
  }
27617
+ if (key.name === "e" && warnings.length > 0) {
27618
+ setModal({ type: "warnings" });
27619
+ return;
27620
+ }
27134
27621
  if (key.name === "n") {
27135
27622
  if (!selectedRow)
27136
27623
  return;
@@ -27154,16 +27641,12 @@ function App({ opts }) {
27154
27641
  const targets = getSelectedRows();
27155
27642
  if (targets.length === 0)
27156
27643
  return;
27157
- if (targets.some((r) => r.isMainCheckout)) {
27158
- flash("Cannot install deps on main checkout");
27159
- return;
27160
- }
27161
27644
  const conflict = findConflict(targets);
27162
27645
  if (conflict) {
27163
27646
  flash(conflict);
27164
27647
  return;
27165
27648
  }
27166
- startBatchActions("install", targets, (r) => ["deps", r.branch, "--repo", r.repoName, "--install"]);
27649
+ startBatchActions("install", targets, (r) => r.isMainCheckout ? ["deps", "--repo", r.repoName, "--install"] : ["deps", r.branch, "--repo", r.repoName, "--install"]);
27167
27650
  return;
27168
27651
  }
27169
27652
  if (key.name === "m") {
@@ -27312,17 +27795,17 @@ function App({ opts }) {
27312
27795
  })();
27313
27796
  }
27314
27797
  });
27315
- return /* @__PURE__ */ jsxs11("box", {
27798
+ return /* @__PURE__ */ jsxs12("box", {
27316
27799
  flexDirection: "column",
27317
27800
  width: "100%",
27318
27801
  height: "100%",
27319
27802
  children: [
27320
- /* @__PURE__ */ jsxs11("box", {
27803
+ /* @__PURE__ */ jsxs12("box", {
27321
27804
  flexDirection: "row",
27322
27805
  width: "100%",
27323
27806
  flexGrow: 1,
27324
27807
  children: [
27325
- /* @__PURE__ */ jsx12(WorktreeTable, {
27808
+ /* @__PURE__ */ jsx13(WorktreeTable, {
27326
27809
  blocks: displayBlocks,
27327
27810
  selectedIndex,
27328
27811
  selection,
@@ -27330,28 +27813,28 @@ function App({ opts }) {
27330
27813
  repoVerbs,
27331
27814
  rowVerbs
27332
27815
  }),
27333
- /* @__PURE__ */ jsx12(DetailPane, {
27816
+ /* @__PURE__ */ jsx13(DetailPane, {
27334
27817
  selectedRow
27335
27818
  })
27336
27819
  ]
27337
27820
  }),
27338
- isFiltering && /* @__PURE__ */ jsxs11("box", {
27821
+ isFiltering && /* @__PURE__ */ jsxs12("box", {
27339
27822
  flexDirection: "row",
27340
27823
  paddingX: 1,
27341
27824
  border: true,
27342
27825
  borderColor: "magenta",
27343
27826
  children: [
27344
- /* @__PURE__ */ jsx12("text", {
27827
+ /* @__PURE__ */ jsx13("text", {
27345
27828
  children: "filter: "
27346
27829
  }),
27347
- /* @__PURE__ */ jsx12("input", {
27830
+ /* @__PURE__ */ jsx13("input", {
27348
27831
  focused: true,
27349
27832
  placeholder: "Type to filter...",
27350
27833
  onInput: (v) => setFilterText(v)
27351
27834
  })
27352
27835
  ]
27353
27836
  }),
27354
- /* @__PURE__ */ jsx12(Footer, {
27837
+ /* @__PURE__ */ jsx13(Footer, {
27355
27838
  loading: loading || refreshing,
27356
27839
  lastRefreshed,
27357
27840
  errorCount: warnings.length,
@@ -27360,13 +27843,16 @@ function App({ opts }) {
27360
27843
  spinnerFrame,
27361
27844
  filter: filterText ? { term: filterText, matches: flatRows.length, total: totalRows } : undefined
27362
27845
  }),
27363
- modal.type === "help" && /* @__PURE__ */ jsx12(HelpOverlay, {}),
27364
- modal.type === "history" && /* @__PURE__ */ jsx12(HistoryOverlay, {}),
27365
- modal.type === "error" && /* @__PURE__ */ jsx12(ConfirmModal, {
27846
+ modal.type === "help" && /* @__PURE__ */ jsx13(HelpOverlay, {}),
27847
+ modal.type === "history" && /* @__PURE__ */ jsx13(HistoryOverlay, {}),
27848
+ modal.type === "warnings" && /* @__PURE__ */ jsx13(WarningsOverlay, {
27849
+ warnings
27850
+ }),
27851
+ modal.type === "error" && /* @__PURE__ */ jsx13(ConfirmModal, {
27366
27852
  title: "Error",
27367
27853
  message: modal.message
27368
27854
  }),
27369
- modal.type === "confirm_remove" && /* @__PURE__ */ jsx12(ConfirmModal, {
27855
+ modal.type === "confirm_remove" && /* @__PURE__ */ jsx13(ConfirmModal, {
27370
27856
  title: `Remove ${modal.rows.length} Worktree(s)`,
27371
27857
  message: (() => {
27372
27858
  const count2 = modal.rows.length;
@@ -27383,15 +27869,15 @@ function App({ opts }) {
27383
27869
  `);
27384
27870
  })()
27385
27871
  }),
27386
- modal.type === "confirm_rebase" && /* @__PURE__ */ jsx12(ConfirmModal, {
27872
+ modal.type === "confirm_rebase" && /* @__PURE__ */ jsx13(ConfirmModal, {
27387
27873
  title: `Rebase ${modal.rows.length} Worktree(s)`,
27388
27874
  message: `Are you sure you want to fetch and rebase ${modal.rows.length === 1 ? modal.rows[0]?.branch : modal.rows.length + " worktrees"}?`
27389
27875
  }),
27390
- modal.type === "confirm_sync" && /* @__PURE__ */ jsx12(ConfirmModal, {
27876
+ modal.type === "confirm_sync" && /* @__PURE__ */ jsx13(ConfirmModal, {
27391
27877
  title: `Sync ${modal.rows.length} Worktree(s)`,
27392
27878
  message: `Are you sure you want to sync ${modal.rows.length === 1 ? modal.rows[0]?.branch : modal.rows.length + " worktrees"}?`
27393
27879
  }),
27394
- createModal && /* @__PURE__ */ jsx12(InputModal, {
27880
+ createModal && /* @__PURE__ */ jsx13(InputModal, {
27395
27881
  title: `New worktree branch in ${selectedRow?.repoName ?? ""}`,
27396
27882
  placeholder: "Branch name (empty to cancel)",
27397
27883
  errorMessage: createError,
@@ -27414,20 +27900,37 @@ function App({ opts }) {
27414
27900
  }
27415
27901
  setCreateModal(false);
27416
27902
  setCreateError(undefined);
27417
- setCreateDepsChoice({ branch, repoName });
27903
+ setCreateBaseError(undefined);
27904
+ setCreateBaseModal({ branch, repoName });
27905
+ }
27906
+ }),
27907
+ createBaseModal && /* @__PURE__ */ jsx13(InputModal, {
27908
+ title: `Base ref for ${createBaseModal.branch}`,
27909
+ placeholder: "origin/main (empty for default main)",
27910
+ errorMessage: createBaseError,
27911
+ onSubmit: (value) => {
27912
+ const base2 = value.trim();
27913
+ if (base2 && !validateSafeBranchName(base2)) {
27914
+ setCreateBaseError("Invalid base ref");
27915
+ return;
27916
+ }
27917
+ const { branch, repoName } = createBaseModal;
27918
+ setCreateBaseModal(null);
27919
+ setCreateBaseError(undefined);
27920
+ setCreateDepsChoice({ branch, repoName, base: base2 || undefined });
27418
27921
  }
27419
27922
  }),
27420
- createDepsChoice && /* @__PURE__ */ jsx12(ChoiceModal, {
27923
+ createDepsChoice && /* @__PURE__ */ jsx13(ChoiceModal, {
27421
27924
  title: `Dependencies for ${createDepsChoice.branch}`,
27422
27925
  options: DEPS_CHOICES,
27423
27926
  onSubmit: (choice) => {
27424
- const { branch, repoName } = createDepsChoice;
27927
+ const { branch, repoName, base: base2 } = createDepsChoice;
27425
27928
  setCreateDepsChoice(null);
27426
- startCreate(branch, repoName, choice);
27929
+ startCreate(branch, repoName, choice, base2);
27427
27930
  },
27428
27931
  onCancel: () => setCreateDepsChoice(null)
27429
27932
  }),
27430
- renameModal && selectedRow && /* @__PURE__ */ jsx12(InputModal, {
27933
+ renameModal && selectedRow && /* @__PURE__ */ jsx13(InputModal, {
27431
27934
  title: `Rename branch ${selectedRow.branch}`,
27432
27935
  placeholder: `New branch name (${selectedRow.branch})`,
27433
27936
  errorMessage: renameError,
@@ -27451,7 +27954,7 @@ function App({ opts }) {
27451
27954
  setModal({ type: "confirm_rename", row: target, to });
27452
27955
  }
27453
27956
  }),
27454
- modal.type === "confirm_rename" && /* @__PURE__ */ jsx12(ConfirmModal, {
27957
+ modal.type === "confirm_rename" && /* @__PURE__ */ jsx13(ConfirmModal, {
27455
27958
  title: "Rename Worktree",
27456
27959
  message: [
27457
27960
  `${modal.row.branch} → ${modal.to}`,
@@ -27464,12 +27967,12 @@ function App({ opts }) {
27464
27967
  ].join(`
27465
27968
  `)
27466
27969
  }),
27467
- configOpen && /* @__PURE__ */ jsx12(ConfigOverlay, {
27970
+ configOpen && /* @__PURE__ */ jsx13(ConfigOverlay, {
27468
27971
  onClose: () => setConfigOpen(false),
27469
27972
  onSaved: () => void doRefresh(),
27470
27973
  onError: (msg) => setModal({ type: "error", message: msg })
27471
27974
  }),
27472
- failedLogs.length > 0 && /* @__PURE__ */ jsx12(ActionLogModal, {
27975
+ failedLogs.length > 0 && /* @__PURE__ */ jsx13(ActionLogModal, {
27473
27976
  title: failedLogs[0].title,
27474
27977
  lines: failedLogs[0].lines,
27475
27978
  done: true,
@@ -27493,6 +27996,8 @@ var init_App = __esm(() => {
27493
27996
  init_InputModal();
27494
27997
  init_ChoiceModal();
27495
27998
  init_ConfigOverlay();
27999
+ init_WarningsOverlay();
28000
+ init_utils();
27496
28001
  init_agents();
27497
28002
  init_config();
27498
28003
  init_history();
@@ -27522,14 +28027,14 @@ __export(exports_tui, {
27522
28027
  });
27523
28028
  import { createCliRenderer, TextTableRenderable } from "@opentui/core";
27524
28029
  import { createRoot, extend as extend2 } from "@opentui/react";
27525
- import { jsx as jsx13 } from "@opentui/react/jsx-runtime";
28030
+ import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
27526
28031
  async function runTerminal(opts) {
27527
28032
  const renderer = await createCliRenderer({
27528
28033
  exitOnCtrlC: false
27529
28034
  });
27530
28035
  const root = createRoot(renderer);
27531
28036
  try {
27532
- root.render(/* @__PURE__ */ jsx13(App, {
28037
+ root.render(/* @__PURE__ */ jsx14(App, {
27533
28038
  opts
27534
28039
  }));
27535
28040
  } catch (err) {
@@ -29661,8 +30166,8 @@ Run 'wtx config show' to see available repos.`);
29661
30166
  init_config();
29662
30167
  init_log();
29663
30168
  init_git();
29664
- import fs20 from "fs";
29665
- import path26 from "path";
30169
+ import fs21 from "fs";
30170
+ import path27 from "path";
29666
30171
 
29667
30172
  // src/lib/remotes.ts
29668
30173
  init_execa();
@@ -29727,6 +30232,7 @@ function resolveBranchTarget(input) {
29727
30232
  }
29728
30233
 
29729
30234
  // src/commands/create.ts
30235
+ init_stack();
29730
30236
  init_resolver();
29731
30237
 
29732
30238
  // src/lib/ide.ts
@@ -29742,14 +30248,14 @@ function spawnIde(ide, wtPath) {
29742
30248
  // src/lib/worktree-setup.ts
29743
30249
  init_execa();
29744
30250
  init_log();
29745
- import fs9 from "fs";
29746
- import path16 from "path";
30251
+ import fs10 from "fs";
30252
+ import path17 from "path";
29747
30253
 
29748
30254
  // src/lib/ports.ts
29749
30255
  init_git();
29750
30256
  init_path_safety();
29751
30257
  init_config();
29752
- import fs8 from "fs";
30258
+ import fs9 from "fs";
29753
30259
  function hashPort(key, min, max) {
29754
30260
  let h2 = 2166136261;
29755
30261
  for (let i2 = 0;i2 < key.length; i2++) {
@@ -29784,7 +30290,7 @@ async function getWorktreePort(repoName, branch, config2, currentWtPath) {
29784
30290
  const taken = new Set;
29785
30291
  for (const name of allRepos) {
29786
30292
  const mainPath = `${root}/${name}`;
29787
- if (!fs8.existsSync(mainPath)) {
30293
+ if (!fs9.existsSync(mainPath)) {
29788
30294
  continue;
29789
30295
  }
29790
30296
  let wts = [];
@@ -29815,12 +30321,12 @@ async function runPostCreateSetup(params) {
29815
30321
  let hooks = [];
29816
30322
  if (repo.config.sync_files && repo.config.sync_files.length > 0) {
29817
30323
  for (const file2 of repo.config.sync_files) {
29818
- const src = path16.join(repo.mainPath, file2);
29819
- const dest = path16.join(wtPath, file2);
29820
- if (fs9.existsSync(src)) {
30324
+ const src = path17.join(repo.mainPath, file2);
30325
+ const dest = path17.join(wtPath, file2);
30326
+ if (fs10.existsSync(src)) {
29821
30327
  if (!globalOpts.dryRun) {
29822
- fs9.mkdirSync(path16.dirname(dest), { recursive: true });
29823
- fs9.copyFileSync(src, dest);
30328
+ fs10.mkdirSync(path17.dirname(dest), { recursive: true });
30329
+ fs10.copyFileSync(src, dest);
29824
30330
  copiedFiles.push(file2);
29825
30331
  }
29826
30332
  stepSuccess(`Synced ${file2}`);
@@ -29948,9 +30454,6 @@ function registerCreateCommand(program2) {
29948
30454
  openWorktree(wtPath);
29949
30455
  continue;
29950
30456
  }
29951
- if (!globalOpts.dryRun) {
29952
- fs20.mkdirSync(path26.dirname(wtPath), { recursive: true });
29953
- }
29954
30457
  const mainBranch = await resolveMainBranch(repo, config2);
29955
30458
  const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
29956
30459
  if (repo.config.fetch_main_on_create) {
@@ -29961,6 +30464,19 @@ function registerCreateCommand(program2) {
29961
30464
  }
29962
30465
  stepProgress("Checking branch status...");
29963
30466
  const baseRef = options.base || `${resolvedRemote}/${mainBranch}`;
30467
+ if (baseRef === branch || baseRef === `refs/heads/${branch}`) {
30468
+ throw new Error(`Base ref '${baseRef}' cannot be the new branch '${branch}'`);
30469
+ }
30470
+ let baseSha = null;
30471
+ if (!globalOpts.dryRun) {
30472
+ baseSha = await resolveCommitSha(repo.mainPath, baseRef, globalOpts);
30473
+ stepSuccess("Base resolved", `${baseRef} at ${baseSha.substring(0, 7)}`);
30474
+ } else {
30475
+ stepProgress("Using base", baseRef);
30476
+ }
30477
+ if (!globalOpts.dryRun) {
30478
+ fs21.mkdirSync(path27.dirname(wtPath), { recursive: true });
30479
+ }
29964
30480
  const localSha = await getLocalBranchSha(repo.mainPath, branch, globalOpts);
29965
30481
  const remoteSha = await getRemoteBranchSha(repo.mainPath, resolvedRemote, branch, globalOpts);
29966
30482
  const localExists = localSha !== null;
@@ -30021,6 +30537,21 @@ function registerCreateCommand(program2) {
30021
30537
  throw err;
30022
30538
  }
30023
30539
  stepSuccess("Worktree created", wtPath);
30540
+ if (!globalOpts.dryRun && baseSha && resolvedAction.kind === "create-new") {
30541
+ try {
30542
+ const metadataBaseRef = options.base ? baseRef : mainBranch;
30543
+ await recordStackEntry(repo.mainPath, branch, {
30544
+ baseRef: metadataBaseRef,
30545
+ baseSha,
30546
+ explicit: options.base !== undefined,
30547
+ createdAt: new Date().toISOString()
30548
+ }, globalOpts);
30549
+ stepSuccess("Base recorded", metadataBaseRef);
30550
+ } catch (err) {
30551
+ const message = err instanceof Error ? err.message : String(err);
30552
+ stepWarning("Base metadata not recorded", message);
30553
+ }
30554
+ }
30024
30555
  const setupResult = await runPostCreateSetup({ config: config2, repo, wtPath, branch, globalOpts });
30025
30556
  if (options.deps && options.deps !== "auto" && options.deps !== "link" && options.deps !== "off") {
30026
30557
  const ok = await applyDepsStrategy(options.deps, wtPath, repo.mainPath, globalOpts);
@@ -30088,9 +30619,10 @@ init_config();
30088
30619
  init_log();
30089
30620
  init_git();
30090
30621
  init_resolver();
30091
- import fs21 from "fs";
30092
- import path27 from "path";
30622
+ import fs22 from "fs";
30623
+ import path28 from "path";
30093
30624
  init_forge();
30625
+ init_stack();
30094
30626
  function registerPullCommand(program2) {
30095
30627
  program2.command("pull <link>").description("Fetch a forge PR and create its worktree").option("-r, --repo <repos...>", "Target specific repo").action(async (link, options) => {
30096
30628
  const globalOpts = program2.opts();
@@ -30112,7 +30644,7 @@ function registerPullCommand(program2) {
30112
30644
  for (const name of Object.keys(config2.repos).sort()) {
30113
30645
  if (config2.repos[name].check_prs === false)
30114
30646
  continue;
30115
- const mainPath = path27.join(expandTilde(config2.root), name);
30647
+ const mainPath = path28.join(expandTilde(config2.root), name);
30116
30648
  const detected = detectRepoForge(mainPath);
30117
30649
  if (!detected)
30118
30650
  continue;
@@ -30210,10 +30742,22 @@ function registerPullCommand(program2) {
30210
30742
  summaryWarning(`Nothing pulled — branch '${branch}' already exists`);
30211
30743
  return;
30212
30744
  }
30213
- const baseRemote = await resolveBaseRemote(target.mainPath, target.config.main_branch === "auto" ? config2.default_main_branch : target.config.main_branch);
30745
+ const mainBranch = await resolveMainBranch(target, config2);
30746
+ const baseRemote = await resolveBaseRemote(target.mainPath, mainBranch);
30747
+ const baseBranch = head.baseRefName || mainBranch;
30748
+ const baseRef = `${baseRemote}/${baseBranch}`;
30214
30749
  const fetch = adapter.buildHeadFetch(head);
30215
30750
  stepProgress(fetch.url ? `Fetching ${fetch.refspec} from fork...` : `Fetching pull/${head.number}/head from ${baseRemote}...`);
30216
30751
  try {
30752
+ if (baseBranch !== mainBranch) {
30753
+ try {
30754
+ await gitExec(["-C", target.mainPath, "fetch", baseRemote, "--", baseBranch], { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun });
30755
+ } catch (err) {
30756
+ const message = err instanceof Error ? err.message : String(err);
30757
+ stepWarning("PR base was not fetched", message.split(`
30758
+ `)[0] ?? message);
30759
+ }
30760
+ }
30217
30761
  await gitExec([
30218
30762
  "-C",
30219
30763
  target.mainPath,
@@ -30227,7 +30771,7 @@ function registerPullCommand(program2) {
30227
30771
  }
30228
30772
  stepSuccess("Fetched");
30229
30773
  if (!globalOpts.dryRun) {
30230
- fs21.mkdirSync(path27.dirname(wtPath), { recursive: true });
30774
+ fs22.mkdirSync(path28.dirname(wtPath), { recursive: true });
30231
30775
  }
30232
30776
  try {
30233
30777
  await gitExec(["-C", target.mainPath, "worktree", "add", "-b", branch, wtPath, "FETCH_HEAD"], { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun });
@@ -30241,6 +30785,22 @@ function registerPullCommand(program2) {
30241
30785
  throw err;
30242
30786
  }
30243
30787
  stepSuccess("Worktree created", wtPath);
30788
+ if (!globalOpts.dryRun) {
30789
+ try {
30790
+ const baseSha = await resolveCommitSha(target.mainPath, baseRef, globalOpts);
30791
+ const metadataBaseRef = baseBranch === mainBranch ? mainBranch : baseRef;
30792
+ await recordStackEntry(target.mainPath, branch, {
30793
+ baseRef: metadataBaseRef,
30794
+ baseSha,
30795
+ explicit: baseBranch !== mainBranch,
30796
+ createdAt: new Date().toISOString()
30797
+ }, globalOpts);
30798
+ stepSuccess("Base recorded", metadataBaseRef);
30799
+ } catch (err) {
30800
+ const message = err instanceof Error ? err.message : String(err);
30801
+ stepWarning("Base metadata not recorded", message);
30802
+ }
30803
+ }
30244
30804
  const setupResult = await runPostCreateSetup({ config: config2, repo: target, wtPath, branch, globalOpts });
30245
30805
  const failedHooks = setupResult.hooks.filter((h2) => !h2.ok);
30246
30806
  if (failedHooks.length > 0) {
@@ -30346,7 +30906,7 @@ init_log();
30346
30906
  init_git();
30347
30907
  init_resolver();
30348
30908
  init_path_safety();
30349
- import path28 from "path";
30909
+ import path29 from "path";
30350
30910
 
30351
30911
  // src/lib/prompts.ts
30352
30912
  import * as readline2 from "readline";
@@ -30389,6 +30949,7 @@ async function confirm(message, io) {
30389
30949
  }
30390
30950
 
30391
30951
  // src/commands/remove.ts
30952
+ init_stack();
30392
30953
  function registerRemoveCommand(program2) {
30393
30954
  program2.command("remove <branch>").description("Remove worktree(s)").option("-r, --repo <repos...>", "Target specific repo(s)").option("-f, --force", "Force removal even if there are uncommitted changes").option("-y, --yes", "Skip confirmation prompt").action(async (branch, options) => {
30394
30955
  const globalOpts = program2.opts();
@@ -30413,6 +30974,16 @@ function registerRemoveCommand(program2) {
30413
30974
  continue;
30414
30975
  }
30415
30976
  const wtPath = target.path;
30977
+ const stackMetadata = await readStackMetadata(repo.mainPath, globalOpts);
30978
+ const children = getStackChildren(stackMetadata, branch);
30979
+ if (children.length > 0 && !options.force) {
30980
+ stepError("Worktree has dependent branches", `${children.join(", ")} — retarget or remove them first (use --force to override)`);
30981
+ skipCount++;
30982
+ continue;
30983
+ }
30984
+ if (children.length > 0) {
30985
+ stepWarning("Removing a parent with dependent branches", children.join(", "));
30986
+ }
30416
30987
  if (!options.force && !globalOpts.dryRun) {
30417
30988
  try {
30418
30989
  const dirtyFiles = await getDirtyFiles(wtPath);
@@ -30443,7 +31014,7 @@ function registerRemoveCommand(program2) {
30443
31014
  const toClean = planEmptyParentRemoval(repo.wtRoot, repo.mainPath, wtPath);
30444
31015
  indented(`Will remove worktree: ${wtPath}`);
30445
31016
  for (const dir of toClean) {
30446
- indented(`Will clean up empty dir: ${path28.relative(repo.wtRoot, dir)}/`);
31017
+ indented(`Will clean up empty dir: ${path29.relative(repo.wtRoot, dir)}/`);
30447
31018
  }
30448
31019
  const proceed = await confirm("Are you sure you want to delete these?");
30449
31020
  if (!proceed) {
@@ -30473,9 +31044,18 @@ function registerRemoveCommand(program2) {
30473
31044
  if (!globalOpts.dryRun) {
30474
31045
  const removedDirs = cleanupEmptyParents(repo.wtRoot, repo.mainPath, wtPath);
30475
31046
  for (const dir of removedDirs) {
30476
- stepSuccess("Cleaned up empty directory", path28.relative(repo.wtRoot, dir) + "/");
31047
+ stepSuccess("Cleaned up empty directory", path29.relative(repo.wtRoot, dir) + "/");
30477
31048
  }
30478
31049
  }
31050
+ try {
31051
+ await removeStackEntry(repo.mainPath, branch, globalOpts);
31052
+ } catch (err) {
31053
+ const message = err instanceof Error ? err.message : String(err);
31054
+ stepWarning("Stack metadata not removed", message);
31055
+ }
31056
+ if (children.length > 0) {
31057
+ stepWarning("Dependent base metadata retained", children.join(", "));
31058
+ }
30479
31059
  successCount++;
30480
31060
  } catch (err) {
30481
31061
  stepError("Failed to remove worktree", err.message);
@@ -30499,7 +31079,7 @@ init_log();
30499
31079
  init_git();
30500
31080
  init_resolver();
30501
31081
  init_forge();
30502
- import path29 from "path";
31082
+ import path30 from "path";
30503
31083
 
30504
31084
  // src/lib/prune.ts
30505
31085
  function selectMergedCandidates(worktrees, mainPath, prMap) {
@@ -30517,6 +31097,7 @@ function selectMergedCandidates(worktrees, mainPath, prMap) {
30517
31097
 
30518
31098
  // src/commands/prune.ts
30519
31099
  init_path_safety();
31100
+ init_stack();
30520
31101
  function registerPruneCommand(program2) {
30521
31102
  program2.command("prune").description("Remove worktrees whose branch has a merged PR").option("-r, --repo <repos...>", "Target specific repo(s)").option("-f, --force", "Remove even if there are uncommitted changes").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
30522
31103
  const globalOpts = program2.opts();
@@ -30540,6 +31121,7 @@ function registerPruneCommand(program2) {
30540
31121
  }
30541
31122
  try {
30542
31123
  const worktrees = await getWorktreeList(repo.mainPath);
31124
+ const stackMetadata = await readStackMetadata(repo.mainPath, globalOpts);
30543
31125
  const branches = worktrees.filter((wt) => wt.path !== repo.mainPath && wt.branch).map((wt) => wt.branch);
30544
31126
  if (branches.length === 0) {
30545
31127
  continue;
@@ -30564,6 +31146,15 @@ function registerPruneCommand(program2) {
30564
31146
  continue;
30565
31147
  }
30566
31148
  const wtInfo = worktrees.find((wt) => wt.path === candidate.path);
31149
+ const children = getStackChildren(stackMetadata, candidate.branch);
31150
+ if (children.length > 0 && !options.force) {
31151
+ stepWarning("Skipped — branch has dependent worktrees", `${label}: ${children.join(", ")}`);
31152
+ skippedCount++;
31153
+ continue;
31154
+ }
31155
+ if (children.length > 0) {
31156
+ stepWarning("Pruning parent with dependent worktrees", `${label}: ${children.join(", ")}`);
31157
+ }
30567
31158
  if (wtInfo?.isLocked && !options.force) {
30568
31159
  stepWarning("Skipped — worktree is locked \uD83D\uDD12", label);
30569
31160
  skippedCount++;
@@ -30644,9 +31235,15 @@ function registerPruneCommand(program2) {
30644
31235
  if (!globalOpts.dryRun) {
30645
31236
  const removedDirs = cleanupEmptyParents(repo.wtRoot, repo.mainPath, candidate.path);
30646
31237
  for (const dir of removedDirs) {
30647
- stepSuccess("Cleaned up empty directory", path29.relative(repo.wtRoot, dir) + "/");
31238
+ stepSuccess("Cleaned up empty directory", path30.relative(repo.wtRoot, dir) + "/");
30648
31239
  }
30649
31240
  }
31241
+ try {
31242
+ await removeStackEntry(repo.mainPath, candidate.branch, globalOpts);
31243
+ } catch (err) {
31244
+ const message = err instanceof Error ? err.message : String(err);
31245
+ stepWarning("Stack metadata not removed", message);
31246
+ }
30650
31247
  removedCount++;
30651
31248
  }
30652
31249
  if (removedCount === 0 && skippedCount === 0) {
@@ -30713,13 +31310,15 @@ function formatRelativeTime(isoTimestamp) {
30713
31310
  // src/commands/ls.ts
30714
31311
  init_owner();
30715
31312
  init_source();
30716
- import path30 from "path";
30717
- import fs22 from "fs";
31313
+ init_stack();
31314
+ init_stack();
31315
+ import path31 from "path";
31316
+ import fs23 from "fs";
30718
31317
  function buildLsJson(reposData) {
30719
31318
  const result = [];
30720
31319
  for (const repo of reposData) {
30721
31320
  for (const wt of repo.worktrees) {
30722
- const branch = wt.branch || path30.basename(wt.path);
31321
+ const branch = wt.branch || path31.basename(wt.path);
30723
31322
  const sha = (wt.commit || "0000000").substring(0, 7);
30724
31323
  let status = "clean";
30725
31324
  if (wt.path === repo.mainPath) {
@@ -30746,6 +31345,8 @@ function buildLsJson(reposData) {
30746
31345
  }
30747
31346
  }
30748
31347
  const entry = { repo: repo.name, branch, sha, status };
31348
+ if (wt.base)
31349
+ entry.base = wt.base;
30749
31350
  if (pr)
30750
31351
  entry.pr = pr;
30751
31352
  if (ownerStr)
@@ -30784,7 +31385,7 @@ function registerLsCommand(program2) {
30784
31385
  };
30785
31386
  try {
30786
31387
  const worktrees = await getWorktreeList(repo.mainPath);
30787
- const maxBranchLen = Math.max(...worktrees.map((wt) => (wt.branch || "main").length));
31388
+ const stackMetadata = await readStackMetadata(repo.mainPath, globalOpts);
30788
31389
  let prMap = null;
30789
31390
  if (options.pr) {
30790
31391
  const branches = worktrees.filter((wt) => wt.path !== repo.mainPath && wt.branch).map((wt) => wt.branch);
@@ -30799,9 +31400,20 @@ function registerLsCommand(program2) {
30799
31400
  }
30800
31401
  }
30801
31402
  }
30802
- for (const wt of worktrees) {
30803
- const branch = wt.branch || path30.basename(wt.path);
30804
- const paddedBranch = branch.padEnd(maxBranchLen + 2);
31403
+ const displayItems = buildStackHierarchy(worktrees, (wt) => wt.branch || path31.basename(wt.path), (wt) => {
31404
+ const branch = wt.branch || path31.basename(wt.path);
31405
+ return stackMetadata.branches[branch]?.baseRef ?? prMap?.get(branch)?.baseRefName;
31406
+ }, (a2, b) => {
31407
+ if (a2.path === repo.mainPath && b.path !== repo.mainPath)
31408
+ return -1;
31409
+ if (a2.path !== repo.mainPath && b.path === repo.mainPath)
31410
+ return 1;
31411
+ return (a2.branch || path31.basename(a2.path)).localeCompare(b.branch || path31.basename(b.path));
31412
+ });
31413
+ const maxBranchLen = Math.max(0, ...displayItems.map(({ item, prefix }) => `${prefix}${item.branch || path31.basename(item.path)}`.length));
31414
+ for (const { item: wt, prefix } of displayItems) {
31415
+ const branch = wt.branch || path31.basename(wt.path);
31416
+ const paddedBranch = `${prefix}${branch}`.padEnd(maxBranchLen + 2);
30805
31417
  const hash2 = (wt.commit || "0000000").substring(0, 7);
30806
31418
  let statusStr = source_default.dim("clean");
30807
31419
  let isMissing = false;
@@ -30811,7 +31423,7 @@ function registerLsCommand(program2) {
30811
31423
  statusStr = source_default.blue("[main checkout]");
30812
31424
  } else if (wt.isLocked) {
30813
31425
  statusStr = source_default.red("locked \uD83D\uDD12");
30814
- } else if (fs22.existsSync(wt.path)) {
31426
+ } else if (fs23.existsSync(wt.path)) {
30815
31427
  try {
30816
31428
  dirtyFiles = await getDirtyFiles(wt.path);
30817
31429
  if (dirtyFiles.length > 0) {
@@ -30829,9 +31441,11 @@ function registerLsCommand(program2) {
30829
31441
  const prInfo = prMap?.get(branch);
30830
31442
  if (prInfo) {
30831
31443
  const display = derivePrDisplay(prInfo);
30832
- prSegment = ` #${prInfo.number} ${renderDisplayState(display)} ${source_default.dim(prInfo.url)}`;
31444
+ prSegment = ` #${prInfo.number} ${renderDisplayState(display)} ${source_default.dim(terminalLink(prInfo.url))}`;
30833
31445
  }
30834
31446
  let ownerSuffix = "";
31447
+ const baseRef = wt.branch ? stackMetadata.branches[wt.branch]?.baseRef ?? prInfo?.baseRefName : prInfo?.baseRefName;
31448
+ const baseSuffix = baseRef ? ` base ${baseRef}` : "";
30835
31449
  let ownership = null;
30836
31450
  if (wt.path !== repo.mainPath) {
30837
31451
  ownership = await resolveOwnership({
@@ -30851,13 +31465,14 @@ function registerLsCommand(program2) {
30851
31465
  path: wt.path,
30852
31466
  branch: wt.branch,
30853
31467
  commit: wt.commit,
31468
+ base: baseRef,
30854
31469
  isLocked: wt.isLocked,
30855
31470
  dirtyFiles,
30856
31471
  isMissing,
30857
31472
  isError
30858
31473
  });
30859
31474
  if (!options.json) {
30860
- info(` ${paddedBranch} ${hash2} ${statusStr}${prSegment}${ownerSuffix}`);
31475
+ info(` ${paddedBranch} ${hash2} ${statusStr}${baseSuffix}${prSegment}${ownerSuffix}`);
30861
31476
  }
30862
31477
  }
30863
31478
  } catch (err) {
@@ -30934,10 +31549,11 @@ end
30934
31549
  init_config();
30935
31550
  init_log();
30936
31551
  init_git();
30937
- import fs23 from "fs";
31552
+ import fs24 from "fs";
30938
31553
  init_resolver();
31554
+ init_stack();
30939
31555
  function registerRebaseCommand(program2) {
30940
- program2.command("rebase <branch>").description("fetch + rebase vs main branch").option("--repo <repos...>", "comma-separated list of repos to target").action(async (branch, _options, cmd) => {
31556
+ program2.command("rebase <branch>").description("fetch + rebase worktree onto its base").option("--repo <repos...>", "comma-separated list of repos to target").option("--onto <ref>", "Override the recorded base ref for this rebase").action(async (branch, options, cmd) => {
30941
31557
  const opts = cmd.optsWithGlobals();
30942
31558
  const config2 = loadConfig();
30943
31559
  const targetRepos = parseRepoFlag(opts.repo);
@@ -30948,30 +31564,58 @@ function registerRebaseCommand(program2) {
30948
31564
  repoHeader(repo.name);
30949
31565
  const mainBranch = await resolveMainBranch(repo, config2);
30950
31566
  const wtPath = getWorktreePath(repo, branch);
30951
- if (!fs23.existsSync(wtPath)) {
31567
+ if (!fs24.existsSync(wtPath)) {
30952
31568
  stepError("No worktree found", `${wtPath} (skipped)`);
30953
31569
  failCount++;
30954
31570
  continue;
30955
31571
  }
30956
31572
  let rebaseStarted = false;
30957
31573
  try {
30958
- const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
30959
- await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch], opts);
30960
- const commit = await getLatestCommit(repo.mainPath, `${resolvedRemote}/${mainBranch}`);
30961
- stepProgress(`Fetching ${resolvedRemote}/${mainBranch}...`, `${commit.hash} "${commit.subject}"`);
30962
- stepProgress(`Rebasing ${branch} onto main...`);
31574
+ const metadata = await readStackMetadata(repo.mainPath, opts);
31575
+ const recorded = metadata.branches[branch];
31576
+ const resolvedRemote = !options.onto && !recorded?.explicit ? await resolveBaseRemote(repo.mainPath, mainBranch) : undefined;
31577
+ const defaultBase = resolvedRemote ? `${resolvedRemote}/${mainBranch}` : mainBranch;
31578
+ const baseRef = options.onto || (recorded?.explicit ? recorded.baseRef : defaultBase);
31579
+ const shouldFetchMain = !options.onto && (!recorded || !recorded.explicit);
31580
+ if (shouldFetchMain) {
31581
+ if (!resolvedRemote) {
31582
+ throw new Error(`Could not determine the remote for base branch '${mainBranch}'`);
31583
+ }
31584
+ await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch], opts);
31585
+ const commit = await getLatestCommit(repo.mainPath, defaultBase);
31586
+ stepProgress(`Fetching ${resolvedRemote}/${mainBranch}...`, `${commit.hash} "${commit.subject}"`);
31587
+ } else {
31588
+ stepProgress("Using recorded base", baseRef);
31589
+ }
31590
+ const baseSha = await resolveCommitSha(repo.mainPath, baseRef, opts);
31591
+ stepProgress(`Rebasing ${branch} onto ${baseRef}...`);
30963
31592
  rebaseStarted = true;
30964
- const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--", `${resolvedRemote}/${mainBranch}`], opts);
31593
+ const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--", baseRef], opts);
30965
31594
  if (rebaseOut.includes("is up to date") || rebaseOut.includes("up-to-date")) {
30966
31595
  stepSuccess("Up to date", "0 commits replayed");
30967
31596
  } else {
30968
- const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${resolvedRemote}/${mainBranch}..HEAD`], opts).then((s) => s.trim());
31597
+ const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${baseRef}..HEAD`], opts).then((s) => s.trim());
30969
31598
  stepSuccess("Rebased", `${count2} commits replayed`);
30970
31599
  }
31600
+ if (recorded || options.onto) {
31601
+ const metadataBaseRef = options.onto || (recorded?.explicit ? baseRef : mainBranch);
31602
+ try {
31603
+ await recordStackEntry(repo.mainPath, branch, {
31604
+ baseRef: metadataBaseRef,
31605
+ baseSha,
31606
+ explicit: options.onto ? true : recorded?.explicit ?? true,
31607
+ createdAt: recorded?.createdAt || new Date().toISOString()
31608
+ }, opts);
31609
+ stepSuccess("Base recorded", metadataBaseRef);
31610
+ } catch (err) {
31611
+ const message = err instanceof Error ? err.message : String(err);
31612
+ stepWarning("Base metadata not updated", message);
31613
+ }
31614
+ }
30971
31615
  successCount++;
30972
31616
  } catch (err) {
30973
31617
  if (!rebaseStarted) {
30974
- stepError("Rebase skipped — could not fetch base branch:", err.message.split(`
31618
+ stepError("Rebase skipped — could not resolve base:", err.message.split(`
30975
31619
  `)[0] ?? err.message);
30976
31620
  failCount++;
30977
31621
  continue;
@@ -31046,8 +31690,8 @@ init_execa();
31046
31690
  init_config();
31047
31691
  init_log();
31048
31692
  init_resolver();
31049
- import fs24 from "fs";
31050
- import path31 from "path";
31693
+ import fs25 from "fs";
31694
+ import path32 from "path";
31051
31695
  function registerSyncCommand(program2) {
31052
31696
  program2.command("sync <branch>").description("re-copy sync files + run post_sync").option("--repo <repos...>", "comma-separated list of repos to target").action(async (branch, _options, cmd) => {
31053
31697
  const opts = cmd.optsWithGlobals();
@@ -31058,19 +31702,19 @@ function registerSyncCommand(program2) {
31058
31702
  for (const repo of repos) {
31059
31703
  repoHeader(repo.name);
31060
31704
  const wtPath = getWorktreePath(repo, branch);
31061
- if (!fs24.existsSync(wtPath)) {
31705
+ if (!fs25.existsSync(wtPath)) {
31062
31706
  stepWarning("No worktree found", `${wtPath} (skipped)`);
31063
31707
  continue;
31064
31708
  }
31065
31709
  try {
31066
31710
  if (repo.config.sync_files) {
31067
31711
  for (const file2 of repo.config.sync_files) {
31068
- const src = path31.join(repo.mainPath, file2);
31069
- const dest = path31.join(wtPath, file2);
31070
- if (fs24.existsSync(src)) {
31712
+ const src = path32.join(repo.mainPath, file2);
31713
+ const dest = path32.join(wtPath, file2);
31714
+ if (fs25.existsSync(src)) {
31071
31715
  if (!opts.dryRun) {
31072
- fs24.mkdirSync(path31.dirname(dest), { recursive: true });
31073
- fs24.copyFileSync(src, dest);
31716
+ fs25.mkdirSync(path32.dirname(dest), { recursive: true });
31717
+ fs25.copyFileSync(src, dest);
31074
31718
  }
31075
31719
  stepSuccess(`Synced ${file2}`);
31076
31720
  }
@@ -31120,15 +31764,15 @@ function registerSyncCommand(program2) {
31120
31764
  process.exit(1);
31121
31765
  }
31122
31766
  }
31123
- const wtNodeModules = path31.join(wtPath, "node_modules");
31124
- if (fs24.existsSync(wtNodeModules) && fs24.lstatSync(wtNodeModules).isSymbolicLink()) {
31767
+ const wtNodeModules = path32.join(wtPath, "node_modules");
31768
+ if (fs25.existsSync(wtNodeModules) && fs25.lstatSync(wtNodeModules).isSymbolicLink()) {
31125
31769
  const lockfiles = ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lockb", "bun.lock"];
31126
31770
  for (const lock of lockfiles) {
31127
- const mainLock = path31.join(repo.mainPath, lock);
31128
- const wtLock = path31.join(wtPath, lock);
31129
- if (fs24.existsSync(mainLock) && fs24.existsSync(wtLock)) {
31130
- const mainContent = fs24.readFileSync(mainLock);
31131
- const wtContent = fs24.readFileSync(wtLock);
31771
+ const mainLock = path32.join(repo.mainPath, lock);
31772
+ const wtLock = path32.join(wtPath, lock);
31773
+ if (fs25.existsSync(mainLock) && fs25.existsSync(wtLock)) {
31774
+ const mainContent = fs25.readFileSync(mainLock);
31775
+ const wtContent = fs25.readFileSync(wtLock);
31132
31776
  if (!mainContent.equals(wtContent)) {
31133
31777
  stepWarning(`${lock} differs from main — node_modules is symlinked`);
31134
31778
  indented(`Run: wtx deps ${branch} --repo ${repo.name} --install`);
@@ -31157,7 +31801,7 @@ init_config();
31157
31801
  init_log();
31158
31802
  init_resolver();
31159
31803
  init_deps();
31160
- import fs25 from "fs";
31804
+ import fs26 from "fs";
31161
31805
  function registerDepsCommand(program2) {
31162
31806
  program2.command("deps [branch]").description("Manage node_modules strategy per worktree").option("-r, --repo <repos...>", "Target specific repo(s)").option("--install", "Switch to independent node_modules (run install)").option("--symlink", "Switch to legacy symlinked node_modules").option("--json", "Output machine-readable JSON state").action(async (branch, options) => {
31163
31807
  const globalOpts = program2.opts();
@@ -31172,16 +31816,29 @@ function registerDepsCommand(program2) {
31172
31816
  if (!options.json)
31173
31817
  repoHeader(repo.name);
31174
31818
  if (!branch) {
31175
- const state = detectDepsState(repo.mainPath, repo.mainPath);
31176
- if (options.json) {
31177
- jsonResults[repo.name] = { main: state };
31819
+ if (options.install) {
31820
+ const ok = await switchToInstall(repo.mainPath, globalOpts);
31821
+ if (!ok)
31822
+ process.exitCode = 1;
31823
+ if (options.json) {
31824
+ jsonResults[repo.name] = detectDepsState(repo.mainPath, repo.mainPath);
31825
+ }
31826
+ } else if (options.symlink) {
31827
+ if (!options.json) {
31828
+ stepWarning("Symlink strategy requires a worktree", "usage: wtx deps <branch> --symlink");
31829
+ }
31178
31830
  } else {
31179
- info(` Main repo package manager: ${state.packageManager ?? "none detected"}`);
31831
+ const state = detectDepsState(repo.mainPath, repo.mainPath);
31832
+ if (options.json) {
31833
+ jsonResults[repo.name] = { main: state };
31834
+ } else {
31835
+ info(` Main repo package manager: ${state.packageManager ?? "none detected"}`);
31836
+ }
31180
31837
  }
31181
31838
  continue;
31182
31839
  }
31183
31840
  const wtPath = getWorktreePath(repo, branch);
31184
- if (!fs25.existsSync(wtPath)) {
31841
+ if (!fs26.existsSync(wtPath)) {
31185
31842
  if (options.json) {
31186
31843
  jsonResults[repo.name] = { error: "No worktree found", branch };
31187
31844
  } else {
@@ -31239,7 +31896,7 @@ function registerDepsCommand(program2) {
31239
31896
  init_config();
31240
31897
  init_log();
31241
31898
  init_resolver();
31242
- import fs26 from "fs";
31899
+ import fs27 from "fs";
31243
31900
  init_git();
31244
31901
  async function resolveMainCheckoutPath(repoCtx, branch) {
31245
31902
  try {
@@ -31264,7 +31921,7 @@ function registerOpenCommand(program2) {
31264
31921
  for (const repo of repos) {
31265
31922
  const wtPath = getWorktreePath(repo, branch);
31266
31923
  let targetPath = wtPath;
31267
- if (!fs26.existsSync(wtPath)) {
31924
+ if (!fs27.existsSync(wtPath)) {
31268
31925
  const mainCheckoutPath = await resolveMainCheckoutPath(repo, branch);
31269
31926
  if (!mainCheckoutPath) {
31270
31927
  continue;
@@ -31294,14 +31951,14 @@ init_config();
31294
31951
  init_log();
31295
31952
  init_git();
31296
31953
  init_resolver();
31297
- import path33 from "path";
31954
+ import path34 from "path";
31298
31955
 
31299
31956
  // src/lib/rename-worktree.ts
31300
31957
  init_git();
31301
31958
  init_resolver();
31302
31959
  init_path_safety();
31303
- import fs27 from "fs";
31304
- import path32 from "path";
31960
+ import fs28 from "fs";
31961
+ import path33 from "path";
31305
31962
  async function getUpstream(wtPath) {
31306
31963
  try {
31307
31964
  const out = await gitExec(["-C", wtPath, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {});
@@ -31321,7 +31978,7 @@ async function planRename(repo, oldBranch, newBranch, opts) {
31321
31978
  throw new Error(`Worktree '${oldBranch}' is locked — unlock it before renaming`);
31322
31979
  }
31323
31980
  const newPath = `${repo.wtRoot}/${newBranch}`;
31324
- if (fs27.existsSync(newPath)) {
31981
+ if (fs28.existsSync(newPath)) {
31325
31982
  throw new Error(`Target path already exists: ${newPath}`);
31326
31983
  }
31327
31984
  if (!opts.dryRun && await localBranchExists(repo.mainPath, newBranch, opts)) {
@@ -31345,7 +32002,7 @@ async function renameWorktree(params) {
31345
32002
  }
31346
32003
  await gitExec(["-C", planned.worktreePath, "branch", "-m", oldBranch, newBranch], opts);
31347
32004
  try {
31348
- fs27.mkdirSync(path32.dirname(planned.newPath), { recursive: true });
32005
+ fs28.mkdirSync(path33.dirname(planned.newPath), { recursive: true });
31349
32006
  await gitExec(["-C", repo.mainPath, "worktree", "move", planned.worktreePath, planned.newPath], opts);
31350
32007
  } catch (err) {
31351
32008
  try {
@@ -31369,6 +32026,7 @@ async function renameWorktree(params) {
31369
32026
  }
31370
32027
 
31371
32028
  // src/commands/rename.ts
32029
+ init_stack();
31372
32030
  function registerRenameCommand(program2) {
31373
32031
  program2.command("rename <old-branch> <new-branch>").description("Rename a worktree's branch and move its checkout to the new location").option("-r, --repo <repos...>", "Target specific repo(s)").action(async (oldBranch, newBranch, options) => {
31374
32032
  const globalOpts = program2.opts();
@@ -31406,12 +32064,19 @@ function registerRenameCommand(program2) {
31406
32064
  stepProgress(`Renaming ${oldBranch} → ${newBranch}...`);
31407
32065
  const outcome = await renameWorktree({ repo, oldBranch, newBranch, opts: globalOpts });
31408
32066
  for (const dir of outcome.cleanedDirs) {
31409
- stepSuccess("Cleaned up empty directory", path33.relative(repo.wtRoot, dir) + "/");
32067
+ stepSuccess("Cleaned up empty directory", path34.relative(repo.wtRoot, dir) + "/");
31410
32068
  }
31411
32069
  stepSuccess("Renamed", `${outcome.oldPath} → ${outcome.newPath}`);
31412
32070
  if (outcome.upstream) {
31413
32071
  indented(`Upstream still tracks '${outcome.upstream}' — after pushing run: git push -u origin ${newBranch}`);
31414
32072
  }
32073
+ try {
32074
+ await renameStackEntry(repo.mainPath, oldBranch, newBranch, globalOpts);
32075
+ stepSuccess("Updated stack metadata", newBranch);
32076
+ } catch (err) {
32077
+ const message = err instanceof Error ? err.message : String(err);
32078
+ stepWarning("Stack metadata not updated", message);
32079
+ }
31415
32080
  summary(`Done — renamed ${oldBranch} to ${newBranch}`);
31416
32081
  } catch (err) {
31417
32082
  stepError("Rename failed", err.message);
@@ -31428,8 +32093,9 @@ init_resolver();
31428
32093
  init_deps();
31429
32094
  init_forge();
31430
32095
  init_types2();
31431
- import fs28 from "fs";
32096
+ import fs29 from "fs";
31432
32097
  init_owner();
32098
+ init_stack();
31433
32099
  init_source();
31434
32100
  function buildStatusJson(item) {
31435
32101
  const entry = {
@@ -31446,11 +32112,14 @@ function buildStatusJson(item) {
31446
32112
  entry.behind = item.behind;
31447
32113
  entry.deps = item.deps;
31448
32114
  if (item.prInfo) {
31449
- entry.pr = {
32115
+ const prEntry = {
31450
32116
  number: item.prInfo.number,
31451
32117
  state: item.prInfo.state,
31452
32118
  url: item.prInfo.url
31453
32119
  };
32120
+ if (item.prInfo.baseRefName)
32121
+ prEntry.base = item.prInfo.baseRefName;
32122
+ entry.pr = prEntry;
31454
32123
  }
31455
32124
  if (item.ownership && !item.ownership.mine && item.ownership.author) {
31456
32125
  entry.owner = item.ownership.author;
@@ -31458,10 +32127,14 @@ function buildStatusJson(item) {
31458
32127
  if (item.rebase) {
31459
32128
  entry.rebase = item.rebase;
31460
32129
  }
32130
+ if (item.base)
32131
+ entry.base = item.base;
32132
+ if (item.baseChanged)
32133
+ entry.baseChanged = true;
31461
32134
  return entry;
31462
32135
  }
31463
32136
  function registerStatusCommand(program2) {
31464
- program2.command("status <branch>").description("Show worktree status across repos").option("-r, --repo <repos...>", "Target specific repo(s)").option("--json", "Output machine-readable JSON").action(async (branch, options) => {
32137
+ program2.command("status <branch>").description("Show worktree status across repos").option("-r, --repo <repos...>", "Target specific repo(s)").option("--json", "Output machine-readable JSON").option("--base <ref>", "Override the recorded base ref for this status check").action(async (branch, options) => {
31465
32138
  const globalOpts = program2.opts();
31466
32139
  const config2 = loadConfig();
31467
32140
  const repoFilter = parseRepoFlag(options.repo);
@@ -31470,17 +32143,33 @@ function registerStatusCommand(program2) {
31470
32143
  const jsonOutputs = [];
31471
32144
  for (const repo of repos) {
31472
32145
  const wtPath = getWorktreePath(repo, branch);
31473
- if (!fs28.existsSync(wtPath)) {
32146
+ if (!fs29.existsSync(wtPath)) {
31474
32147
  continue;
31475
32148
  }
31476
32149
  found++;
31477
32150
  const dirtyFiles = await getDirtyFiles(wtPath);
31478
32151
  let ahead = null;
31479
32152
  let behind = null;
32153
+ let baseRef;
32154
+ let baseChanged = false;
32155
+ let usingStackBase = false;
31480
32156
  try {
31481
32157
  const mainBranch = await resolveMainBranch(repo, config2);
31482
- const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
31483
- const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${resolvedRemote}/${mainBranch}...HEAD`], { verbose: globalOpts.verbose });
32158
+ const metadata = await readStackMetadata(repo.mainPath, globalOpts);
32159
+ const recorded = metadata.branches[branch];
32160
+ const resolvedRemote = !options.base && !recorded?.explicit ? await resolveBaseRemote(repo.mainPath, mainBranch) : undefined;
32161
+ const defaultBase = resolvedRemote ? `${resolvedRemote}/${mainBranch}` : mainBranch;
32162
+ baseRef = options.base || (recorded?.explicit ? recorded.baseRef : defaultBase);
32163
+ usingStackBase = Boolean(options.base || recorded?.explicit);
32164
+ if (recorded?.explicit && !options.base && !globalOpts.dryRun) {
32165
+ try {
32166
+ const currentBaseSha = await resolveCommitSha(repo.mainPath, recorded.baseRef, globalOpts);
32167
+ baseChanged = currentBaseSha !== recorded.baseSha;
32168
+ } catch {
32169
+ baseChanged = true;
32170
+ }
32171
+ }
32172
+ const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${baseRef}...HEAD`], { verbose: globalOpts.verbose });
31484
32173
  const parts = countOutput.trim().split(/\s+/);
31485
32174
  behind = parts[0] ? parseInt(parts[0], 10) : null;
31486
32175
  ahead = parts[1] ? parseInt(parts[1], 10) : null;
@@ -31519,7 +32208,9 @@ function registerStatusCommand(program2) {
31519
32208
  prInfo,
31520
32209
  ownership,
31521
32210
  deps: depsState,
31522
- rebase: rebaseStatus
32211
+ rebase: rebaseStatus,
32212
+ base: usingStackBase ? baseRef : undefined,
32213
+ baseChanged
31523
32214
  }));
31524
32215
  continue;
31525
32216
  }
@@ -31533,10 +32224,16 @@ function registerStatusCommand(program2) {
31533
32224
  indented(` ${f}`);
31534
32225
  }
31535
32226
  }
32227
+ if (usingStackBase && baseRef) {
32228
+ info(` Base: ${baseRef}`);
32229
+ if (baseChanged) {
32230
+ info(` Base state: moved since stack entry`);
32231
+ }
32232
+ }
31536
32233
  if (ahead !== null && behind !== null) {
31537
- info(` vs main: ${ahead} ahead, ${behind} behind`);
32234
+ info(` ${usingStackBase ? "vs base" : "vs main"}: ${ahead} ahead, ${behind} behind`);
31538
32235
  } else {
31539
- info(` vs main: unknown`);
32236
+ info(` ${usingStackBase ? "vs base" : "vs main"}: unknown`);
31540
32237
  }
31541
32238
  if (prInfo) {
31542
32239
  const display = derivePrDisplay(prInfo);
@@ -31583,6 +32280,7 @@ init_git();
31583
32280
  init_resolver();
31584
32281
  init_forge();
31585
32282
  init_owner();
32283
+ init_stack();
31586
32284
  init_types2();
31587
32285
  var ATTENTION_STATES = new Set([
31588
32286
  PR_DISPLAY_STATES.CONFLICTED,
@@ -31614,6 +32312,7 @@ async function collectPrRows(repos, config2, verboseFlag) {
31614
32312
  continue;
31615
32313
  try {
31616
32314
  const worktrees = await getWorktreeList(repo.mainPath);
32315
+ const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: verboseFlag, dryRun: false });
31617
32316
  const branches = worktrees.filter((wt) => wt.path !== repo.mainPath && wt.branch).map((wt) => wt.branch);
31618
32317
  if (branches.length === 0)
31619
32318
  continue;
@@ -31643,7 +32342,8 @@ async function collectPrRows(repos, config2, verboseFlag) {
31643
32342
  unresolvedThreads: pr.unresolvedThreads,
31644
32343
  updatedAt: pr.updatedAt,
31645
32344
  authorLogin: pr.authorLogin ?? null,
31646
- ownership
32345
+ ownership,
32346
+ baseRef: pr.baseRefName ?? stackMetadata.branches[branch]?.baseRef ?? null
31647
32347
  });
31648
32348
  }
31649
32349
  } catch (err) {
@@ -31669,8 +32369,9 @@ function renderTable(rows) {
31669
32369
  const threads = row.unresolvedThreads > 0 ? `${row.unresolvedThreads} thread${row.unresolvedThreads > 1 ? "s" : ""}` : null;
31670
32370
  const details = [row.checksSummary, threads].filter(Boolean).join(" · ");
31671
32371
  const detailSuffix = details ? ` ${details}` : "";
32372
+ const baseSuffix = row.baseRef ? ` → ${row.baseRef}` : "";
31672
32373
  const authorTag = row.ownership && !row.ownership.mine && row.ownership.author ? ` ${source_default.dim(row.ownership.author)}` : "";
31673
- info(` #${row.prNumber} ${paddedBranch} ${renderDisplayState(row.prDisplay)}${detailSuffix}${authorTag} ${formatRelativeTime(row.updatedAt)} ${source_default.dim(row.url)}`);
32374
+ info(` #${row.prNumber} ${paddedBranch} ${renderDisplayState(row.prDisplay)}${baseSuffix}${detailSuffix}${authorTag} ${formatRelativeTime(row.updatedAt)} ${source_default.dim(terminalLink(row.url))}`);
31674
32375
  }
31675
32376
  }
31676
32377
  }
@@ -31683,7 +32384,8 @@ function toJsonOutput(rows) {
31683
32384
  awaitingReview: row.prDisplay.awaitingReview,
31684
32385
  approved: row.prDisplay.approved,
31685
32386
  prNumber: row.prNumber,
31686
- author: row.authorLogin
32387
+ author: row.authorLogin,
32388
+ base: row.baseRef
31687
32389
  }));
31688
32390
  }
31689
32391
  function registerPrsCommand(program2) {
@@ -31741,8 +32443,8 @@ function registerPrsCommand(program2) {
31741
32443
 
31742
32444
  // src/commands/skill.ts
31743
32445
  init_log();
31744
- import fs29 from "fs";
31745
- import path34 from "path";
32446
+ import fs30 from "fs";
32447
+ import path35 from "path";
31746
32448
  import { URL as URL2 } from "url";
31747
32449
  var __dirname2 = new URL2(".", import.meta.url).pathname;
31748
32450
  var COMMON_MARKDOWN = `# wtx — Worktree Manager
@@ -31771,8 +32473,12 @@ Lists all worktrees across repositories.
31771
32473
  Shows git statuses for all worktrees.
31772
32474
 
31773
32475
  ### \`wtx rebase <branch>\`
31774
- Fetches the main branch from origin and rebases the given worktree's branch onto it.
31775
- - **Flags**: \`-r, --repo <repos...>\`
32476
+ Fetches the configured main branch for an independent worktree, or rebases onto its recorded base for a stacked worktree.
32477
+ - **Flags**: \`-r, --repo <repos...>\`, \`--onto <ref>\` to override the base.
32478
+
32479
+ ### \`wtx stack <branch>\`
32480
+ Shows the recorded parent and descendant branches for a worktree.
32481
+ - **Flags**: \`-r, --repo <repos...>\`, \`--json\`
31776
32482
 
31777
32483
  ### \`wtx sync <branch>\`
31778
32484
  Re-copies \`sync_files\` from the main checkout to the worktree and runs \`post_sync\` hooks. Also checks for package lockfile differences if \`node_modules\` is symlinked.
@@ -31837,15 +32543,23 @@ Hooks (\`post_create\` and \`post_sync\`) support the following template variabl
31837
32543
  \`\`\`bash
31838
32544
  wtx rebase feature-xyz
31839
32545
  \`\`\`
31840
- *Fetches the latest main branch from origin and rebases the \`feature-xyz\` worktrees.*
32546
+ *Fetches the latest main branch for independent work, or uses the recorded parent for a stacked branch.*
32547
+
32548
+ 3. **Creating a stacked branch**:
32549
+ \`\`\`bash
32550
+ wtx create feature-api
32551
+ wtx create feature-ui --base feature-api
32552
+ wtx stack feature-ui
32553
+ \`\`\`
32554
+ *Open the child PR against \`feature-api\`; after the parent merges, retarget the child to main before rebasing it onto main.*
31841
32555
 
31842
- 3. **Syncing environment variables**:
32556
+ 4. **Syncing environment variables**:
31843
32557
  If the \`.env\` file in the main checkout was updated, run:
31844
32558
  \`\`\`bash
31845
32559
  wtx sync feature-xyz
31846
32560
  \`\`\`
31847
32561
 
31848
- 4. **Managing node_modules dependencies**:
32562
+ 5. **Managing node_modules dependencies**:
31849
32563
  If a worktree requires different dependencies than the main branch (e.g. you're testing an upgrade):
31850
32564
  \`\`\`bash
31851
32565
  wtx deps feature-xyz --install
@@ -31884,9 +32598,9 @@ function registerSkillCommand(program2) {
31884
32598
  error51(`Unknown platform: ${platform2}`);
31885
32599
  process.exit(1);
31886
32600
  }
31887
- let projectRoot = path34.resolve(__dirname2, "..", "..");
31888
- let skillPath = path34.join(projectRoot, "skills", `${platform2}.md`);
31889
- if (fs29.existsSync(skillPath)) {
32601
+ let projectRoot = path35.resolve(__dirname2, "..", "..");
32602
+ let skillPath = path35.join(projectRoot, "skills", `${platform2}.md`);
32603
+ if (fs30.existsSync(skillPath)) {
31890
32604
  process.stdout.write(skillPath + `
31891
32605
  `);
31892
32606
  } else {
@@ -31932,7 +32646,8 @@ init_git();
31932
32646
  init_deps();
31933
32647
  import readline3 from "readline";
31934
32648
  init_path_safety();
31935
- import fs31 from "fs";
32649
+ init_stack();
32650
+ import fs32 from "fs";
31936
32651
  async function runMcpServer(opts = {}) {
31937
32652
  const input = opts.input ?? process.stdin;
31938
32653
  const output = opts.output ?? process.stdout;
@@ -32025,7 +32740,7 @@ async function handleRequest(req, send, config2, opts) {
32025
32740
  description: "Get status of a specific worktree",
32026
32741
  inputSchema: {
32027
32742
  type: "object",
32028
- properties: { repo: { type: "string" }, branch: { type: "string" } },
32743
+ properties: { repo: { type: "string" }, branch: { type: "string" }, base: { type: "string" } },
32029
32744
  required: ["repo", "branch"]
32030
32745
  }
32031
32746
  },
@@ -32049,10 +32764,10 @@ async function handleRequest(req, send, config2, opts) {
32049
32764
  },
32050
32765
  {
32051
32766
  name: "rebase_worktree",
32052
- description: "Rebase a worktree against main branch",
32767
+ description: "Rebase a worktree against its recorded base or an explicit ref",
32053
32768
  inputSchema: {
32054
32769
  type: "object",
32055
- properties: { repo: { type: "string" }, branch: { type: "string" } },
32770
+ properties: { repo: { type: "string" }, branch: { type: "string" }, onto: { type: "string" } },
32056
32771
  required: ["repo", "branch"]
32057
32772
  }
32058
32773
  }
@@ -32111,16 +32826,18 @@ async function handleToolCall(name, args, config2, _opts) {
32111
32826
  const items = [];
32112
32827
  for (const repo of repos) {
32113
32828
  const wts = await getWorktreeList(repo.mainPath);
32829
+ const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
32114
32830
  for (const wt of wts) {
32115
32831
  if (!wt.path.startsWith(repo.wtRoot))
32116
32832
  continue;
32117
- const dirtyCount = fs31.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
32833
+ const dirtyCount = fs32.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
32118
32834
  items.push({
32119
32835
  repo: repo.name,
32120
32836
  branch: wt.branch || null,
32121
32837
  path: wt.path,
32122
32838
  sha: wt.commit ? wt.commit.substring(0, 7) : null,
32123
- dirtyFiles: dirtyCount
32839
+ dirtyFiles: dirtyCount,
32840
+ base: wt.branch && stackMetadata.branches[wt.branch]?.explicit ? stackMetadata.branches[wt.branch]?.baseRef : null
32124
32841
  });
32125
32842
  }
32126
32843
  }
@@ -32133,18 +32850,27 @@ async function handleToolCall(name, args, config2, _opts) {
32133
32850
  if (!validateSafeBranchName(args.branch)) {
32134
32851
  throw { isToolError: true, message: "unsafe branch name" };
32135
32852
  }
32853
+ if (args.base !== undefined && typeof args.base !== "string") {
32854
+ throw { isSchemaError: true, message: "base must be a string" };
32855
+ }
32856
+ if (typeof args.base === "string" && !validateSafeBranchName(args.base)) {
32857
+ throw { isToolError: true, message: "unsafe base ref" };
32858
+ }
32136
32859
  const repo = resolveRepos(config2, [args.repo])[0];
32137
32860
  const wtPath = getWorktreePath(repo, args.branch);
32138
- if (!fs31.existsSync(wtPath)) {
32861
+ if (!fs32.existsSync(wtPath)) {
32139
32862
  throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
32140
32863
  }
32141
32864
  const mainBranch = await resolveMainBranch(repo, config2);
32142
32865
  const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
32866
+ const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
32867
+ const recorded = stackMetadata.branches[args.branch];
32868
+ const baseRef = typeof args.base === "string" ? args.base : recorded?.explicit ? recorded.baseRef : `${resolvedRemote}/${mainBranch}`;
32143
32869
  const dirtyFiles = await getDirtyFiles(wtPath);
32144
32870
  let ahead = null;
32145
32871
  let behind = null;
32146
32872
  try {
32147
- const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${resolvedRemote}/${mainBranch}...HEAD`]);
32873
+ const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${baseRef}...HEAD`]);
32148
32874
  const parts = countOutput.trim().split(/\s+/);
32149
32875
  behind = parts[0] ? parseInt(parts[0], 10) : null;
32150
32876
  ahead = parts[1] ? parseInt(parts[1], 10) : null;
@@ -32160,6 +32886,7 @@ async function handleToolCall(name, args, config2, _opts) {
32160
32886
  dirtyCount: dirtyFiles.length,
32161
32887
  ahead,
32162
32888
  behind,
32889
+ base: recorded?.explicit || typeof args.base === "string" ? baseRef : null,
32163
32890
  depsStrategy: depsState.strategy
32164
32891
  })
32165
32892
  }]
@@ -32187,9 +32914,11 @@ async function handleToolCall(name, args, config2, _opts) {
32187
32914
  }
32188
32915
  const mainBranch = await resolveMainBranch(repo, config2);
32189
32916
  const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
32917
+ const baseRef = args.base || `${resolvedRemote}/${mainBranch}`;
32190
32918
  if (repo.config.fetch_main_on_create !== false) {
32191
32919
  await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch]);
32192
32920
  }
32921
+ const baseSha = await resolveCommitSha(repo.mainPath, baseRef, { verbose: _opts.verbose === true, dryRun: false });
32193
32922
  const localExists = await localBranchExists(repo.mainPath, args.branch, { verbose: false, dryRun: false });
32194
32923
  const remoteExists = await branchExistsOnRemote(repo.mainPath, args.branch, { verbose: false, dryRun: false }, resolvedRemote);
32195
32924
  const localSha = localExists ? await getLocalBranchSha(repo.mainPath, args.branch, { verbose: false, dryRun: false }) : null;
@@ -32207,6 +32936,14 @@ async function handleToolCall(name, args, config2, _opts) {
32207
32936
  gitArgs.push(wtPath, args.branch);
32208
32937
  }
32209
32938
  await gitExec(gitArgs);
32939
+ if (resolution.kind === "create-new") {
32940
+ await recordStackEntry(repo.mainPath, args.branch, {
32941
+ baseRef: args.base || mainBranch,
32942
+ baseSha,
32943
+ explicit: args.base !== undefined,
32944
+ createdAt: new Date().toISOString()
32945
+ }, { verbose: _opts.verbose === true, dryRun: false });
32946
+ }
32210
32947
  return {
32211
32948
  content: [{ type: "text", text: JSON.stringify({ path: wtPath }) }]
32212
32949
  };
@@ -32235,7 +32972,12 @@ async function handleToolCall(name, args, config2, _opts) {
32235
32972
  throw { isToolError: true, message: `Worktree for ${args.branch} is not registered` };
32236
32973
  }
32237
32974
  const wtPath = target.path;
32238
- if (!args.force && fs31.existsSync(wtPath)) {
32975
+ const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
32976
+ const children = getStackChildren(stackMetadata, args.branch);
32977
+ if (children.length > 0 && args.force !== true) {
32978
+ throw { isToolError: true, message: `Branch has dependent worktrees: ${children.join(", ")}. Use force:true to override.` };
32979
+ }
32980
+ if (!args.force && fs32.existsSync(wtPath)) {
32239
32981
  const dirty = await getDirtyFiles(wtPath);
32240
32982
  if (dirty.length > 0) {
32241
32983
  throw { isToolError: true, message: `Worktree is dirty. Use force:true to remove it.` };
@@ -32243,6 +32985,7 @@ async function handleToolCall(name, args, config2, _opts) {
32243
32985
  }
32244
32986
  await gitExec(["-C", repo.mainPath, "worktree", "remove", args.force ? "--force" : "", wtPath].filter(Boolean));
32245
32987
  cleanupEmptyParents(repo.wtRoot, repo.mainPath, wtPath);
32988
+ await removeStackEntry(repo.mainPath, args.branch, { verbose: _opts.verbose === true, dryRun: false });
32246
32989
  return {
32247
32990
  content: [{ type: "text", text: JSON.stringify({ removed: true }) }]
32248
32991
  };
@@ -32253,18 +32996,46 @@ async function handleToolCall(name, args, config2, _opts) {
32253
32996
  }
32254
32997
  const repo = resolveRepos(config2, [args.repo])[0];
32255
32998
  const wtPath = getWorktreePath(repo, args.branch);
32256
- if (!fs31.existsSync(wtPath)) {
32999
+ if (!fs32.existsSync(wtPath)) {
32257
33000
  throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
32258
33001
  }
32259
33002
  const mainBranch = await resolveMainBranch(repo, config2);
32260
33003
  const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
32261
- await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch]);
33004
+ if (args.onto !== undefined && typeof args.onto !== "string") {
33005
+ throw { isSchemaError: true, message: "onto must be a string" };
33006
+ }
33007
+ if (typeof args.onto === "string" && !validateSafeBranchName(args.onto)) {
33008
+ throw { isToolError: true, message: "unsafe base ref" };
33009
+ }
33010
+ const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
33011
+ const recorded = stackMetadata.branches[args.branch];
33012
+ const baseRef = args.onto || (recorded?.explicit ? recorded.baseRef : `${resolvedRemote}/${mainBranch}`);
33013
+ if (!args.onto && (!recorded || !recorded.explicit)) {
33014
+ await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch]);
33015
+ }
33016
+ const baseSha = await resolveCommitSha(repo.mainPath, baseRef, { verbose: _opts.verbose === true, dryRun: false });
32262
33017
  try {
32263
- const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--", `${resolvedRemote}/${mainBranch}`]);
33018
+ const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--", baseRef]);
32264
33019
  if (rebaseOut.includes("is up to date") || rebaseOut.includes("up-to-date")) {
33020
+ if (recorded || args.onto) {
33021
+ await recordStackEntry(repo.mainPath, args.branch, {
33022
+ baseRef: args.onto || (recorded?.explicit ? baseRef : mainBranch),
33023
+ baseSha,
33024
+ explicit: args.onto ? true : recorded?.explicit ?? true,
33025
+ createdAt: recorded?.createdAt || new Date().toISOString()
33026
+ }, { verbose: _opts.verbose === true, dryRun: false });
33027
+ }
32265
33028
  return { content: [{ type: "text", text: JSON.stringify({ status: "up-to-date" }) }] };
32266
33029
  } else {
32267
- const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${resolvedRemote}/${mainBranch}..HEAD`]).then((s) => s.trim());
33030
+ const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${baseRef}..HEAD`]).then((s) => s.trim());
33031
+ if (recorded || args.onto) {
33032
+ await recordStackEntry(repo.mainPath, args.branch, {
33033
+ baseRef: args.onto || (recorded?.explicit ? baseRef : mainBranch),
33034
+ baseSha,
33035
+ explicit: args.onto ? true : recorded?.explicit ?? true,
33036
+ createdAt: recorded?.createdAt || new Date().toISOString()
33037
+ }, { verbose: _opts.verbose === true, dryRun: false });
33038
+ }
32268
33039
  return { content: [{ type: "text", text: JSON.stringify({ status: "rebased", commits: count2 }) }] };
32269
33040
  }
32270
33041
  } catch (err) {
@@ -32354,12 +33125,100 @@ function registerHistoryCommand(program2) {
32354
33125
  });
32355
33126
  }
32356
33127
 
33128
+ // src/commands/stack.ts
33129
+ init_config();
33130
+ init_git();
33131
+ init_resolver();
33132
+ init_stack();
33133
+ init_log();
33134
+ function nodeFor(branch, metadata, worktrees) {
33135
+ const entry = metadata.branches[branch];
33136
+ const worktree = worktrees.find((wt) => wt.branch === branch)?.path ?? null;
33137
+ return {
33138
+ branch,
33139
+ base: entry?.baseRef ?? null,
33140
+ explicit: entry?.explicit ?? false,
33141
+ baseSha: entry?.baseSha ?? null,
33142
+ worktree
33143
+ };
33144
+ }
33145
+ function collectDescendants(metadata, branch, worktrees, seen) {
33146
+ const nodes = [];
33147
+ for (const child of getStackChildren(metadata, branch)) {
33148
+ if (seen.has(child))
33149
+ continue;
33150
+ seen.add(child);
33151
+ nodes.push(nodeFor(child, metadata, worktrees));
33152
+ nodes.push(...collectDescendants(metadata, child, worktrees, seen));
33153
+ }
33154
+ return nodes;
33155
+ }
33156
+ function renderNode(node, prefix) {
33157
+ const base2 = node.base ? ` base ${node.base}` : " base not recorded";
33158
+ const location = node.worktree ? ` ${node.worktree}` : " no local worktree";
33159
+ info(` ${prefix}${node.branch}`);
33160
+ indented(`${" ".repeat(prefix.length)}${base2} · ${location}`);
33161
+ }
33162
+ function registerStackCommand(program2) {
33163
+ program2.command("stack <branch>").description("Show the recorded branch stack").option("-r, --repo <repos...>", "Target specific repo(s)").option("--json", "Output machine-readable JSON").action(async (branch, options) => {
33164
+ const globalOpts = program2.opts();
33165
+ if (!validateSafeBranchName(branch)) {
33166
+ stepError(`Invalid branch name: '${branch}'`);
33167
+ process.exit(1);
33168
+ }
33169
+ const config2 = loadConfig();
33170
+ const repoFilter = parseRepoFlag(options.repo);
33171
+ const repos = resolveRepos(config2, repoFilter);
33172
+ const jsonResults = [];
33173
+ let successCount = 0;
33174
+ for (const repo of repos) {
33175
+ try {
33176
+ const metadata = await readStackMetadata(repo.mainPath, globalOpts);
33177
+ const worktrees = await getWorktreeList(repo.mainPath);
33178
+ const ancestors = getStackAncestors(metadata, branch);
33179
+ const nodes = ancestors.map((item) => nodeFor(item, metadata, worktrees));
33180
+ const descendants = collectDescendants(metadata, branch, worktrees, new Set(ancestors));
33181
+ const allNodes = [...nodes, ...descendants];
33182
+ const displayNodes = buildStackHierarchy(allNodes, (node) => node.branch, (node) => node.base ?? undefined, (a2, b) => a2.branch.localeCompare(b.branch));
33183
+ if (options.json) {
33184
+ jsonResults.push({ repo: repo.name, branch, nodes: allNodes });
33185
+ } else {
33186
+ repoHeader(repo.name);
33187
+ info(` Stack for ${branch}`);
33188
+ if (allNodes.length === 1 && !metadata.branches[branch]) {
33189
+ const mainBranch = await resolveMainBranch(repo, config2);
33190
+ const defaultRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
33191
+ const defaultBase = `${defaultRemote}/${mainBranch}`;
33192
+ indented(`No recorded parent; default base is ${defaultBase}`);
33193
+ }
33194
+ displayNodes.forEach(({ item, prefix }) => renderNode(item, prefix));
33195
+ }
33196
+ successCount++;
33197
+ } catch (err) {
33198
+ const message = err instanceof Error ? err.message : String(err);
33199
+ if (options.json) {
33200
+ console.error(JSON.stringify({ repo: repo.name, error: message }));
33201
+ } else {
33202
+ stepError("Failed to read stack", `${repo.name}: ${message}`);
33203
+ }
33204
+ }
33205
+ }
33206
+ if (options.json) {
33207
+ console.log(JSON.stringify(jsonResults.length === 1 ? jsonResults[0] : jsonResults, null, 2));
33208
+ } else if (successCount === 0) {
33209
+ summaryWarning("No stacks found");
33210
+ } else {
33211
+ summary(`Done — ${successCount} repo${successCount > 1 ? "s" : ""} checked`);
33212
+ }
33213
+ });
33214
+ }
33215
+
32357
33216
  // src/index.ts
32358
33217
  init_config();
32359
33218
  init_resolver();
32360
33219
  init_log();
32361
33220
  init_history();
32362
- import fs32 from "fs";
33221
+ import fs33 from "fs";
32363
33222
  var program2 = new Command;
32364
33223
  program2.name("wtx").description("Multi-repo git worktree manager").version(VERSION).option("-q, --quiet", "Suppress progress indicators", false).option("--verbose", "Show git commands as they run", false).option("--dry-run", "Show what would happen", false);
32365
33224
  var MUTATING_COMMANDS = new Set([
@@ -32451,6 +33310,7 @@ registerSkillCommand(program2);
32451
33310
  registerTerminalCommand(program2);
32452
33311
  registerMcpCommand(program2);
32453
33312
  registerHistoryCommand(program2);
33313
+ registerStackCommand(program2);
32454
33314
  program2.command("_resolve-path <repo> <branch>", { hidden: true }).description("Internal command to resolve worktree path for shell wrapper cd").action((repoName, branch) => {
32455
33315
  try {
32456
33316
  const config2 = loadConfig();
@@ -32462,7 +33322,7 @@ program2.command("_resolve-path <repo> <branch>", { hidden: true }).description(
32462
33322
  }
32463
33323
  const repo = repos[0];
32464
33324
  const wtPath = getWorktreePath(repo, branch);
32465
- if (!fs32.existsSync(wtPath)) {
33325
+ if (!fs33.existsSync(wtPath)) {
32466
33326
  process.stderr.write(`✗ No worktree at ${wtPath}
32467
33327
  `);
32468
33328
  process.exit(1);