@ogpoyraz/wtx 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/completions/_wtx.zsh +14 -2
- package/completions/wtx.bash +6 -5
- package/completions/wtx.fish +10 -5
- package/dist/cli.mjs +917 -265
- package/package.json +1 -1
- package/skills/claude.md +18 -6
- package/skills/cursor.md +18 -6
- package/skills/opencode.md +18 -6
package/dist/cli.mjs
CHANGED
|
@@ -23014,6 +23014,20 @@ async function getLatestCommit(repoPath, ref) {
|
|
|
23014
23014
|
subject: stdout.substring(splitIdx + 1)
|
|
23015
23015
|
};
|
|
23016
23016
|
}
|
|
23017
|
+
async function resolveCommitSha(repoPath, ref, opts = { verbose: false, dryRun: false }) {
|
|
23018
|
+
try {
|
|
23019
|
+
const stdout = await gitExec(["-C", repoPath, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], opts);
|
|
23020
|
+
const sha = stdout.trim();
|
|
23021
|
+
if (!sha) {
|
|
23022
|
+
throw new Error("Git returned an empty commit id");
|
|
23023
|
+
}
|
|
23024
|
+
return sha;
|
|
23025
|
+
} catch (err) {
|
|
23026
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23027
|
+
throw new Error(`Ref '${ref}' does not resolve to a commit: ${message.split(`
|
|
23028
|
+
`)[0]}`);
|
|
23029
|
+
}
|
|
23030
|
+
}
|
|
23017
23031
|
async function getDirtyFiles(worktreePath) {
|
|
23018
23032
|
const stdout = await gitExec(["-C", worktreePath, "status", "--porcelain"]);
|
|
23019
23033
|
return stdout.split(`
|
|
@@ -23131,19 +23145,195 @@ var init_git = __esm(() => {
|
|
|
23131
23145
|
init_log();
|
|
23132
23146
|
});
|
|
23133
23147
|
|
|
23134
|
-
// src/lib/
|
|
23148
|
+
// src/lib/stack.ts
|
|
23135
23149
|
import fs7 from "fs";
|
|
23136
23150
|
import path15 from "path";
|
|
23151
|
+
function emptyMetadata() {
|
|
23152
|
+
return { version: 1, branches: {} };
|
|
23153
|
+
}
|
|
23154
|
+
function isRecord(value) {
|
|
23155
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23156
|
+
}
|
|
23157
|
+
function parseMetadata(value) {
|
|
23158
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.branches)) {
|
|
23159
|
+
throw new Error("invalid format or version");
|
|
23160
|
+
}
|
|
23161
|
+
const branches = {};
|
|
23162
|
+
for (const [branch, rawEntry] of Object.entries(value.branches)) {
|
|
23163
|
+
if (!isRecord(rawEntry))
|
|
23164
|
+
continue;
|
|
23165
|
+
if (typeof rawEntry.baseRef !== "string" || typeof rawEntry.baseSha !== "string")
|
|
23166
|
+
continue;
|
|
23167
|
+
branches[branch] = {
|
|
23168
|
+
baseRef: rawEntry.baseRef,
|
|
23169
|
+
baseSha: rawEntry.baseSha,
|
|
23170
|
+
explicit: rawEntry.explicit === true,
|
|
23171
|
+
createdAt: typeof rawEntry.createdAt === "string" ? rawEntry.createdAt : ""
|
|
23172
|
+
};
|
|
23173
|
+
}
|
|
23174
|
+
return { version: 1, branches };
|
|
23175
|
+
}
|
|
23176
|
+
async function metadataPath(repoPath, opts) {
|
|
23177
|
+
const commonDirOutput = await gitExec(["-C", repoPath, "rev-parse", "--git-common-dir"], { ...opts, dryRun: false });
|
|
23178
|
+
const commonDir = commonDirOutput.trim();
|
|
23179
|
+
if (!commonDir)
|
|
23180
|
+
return null;
|
|
23181
|
+
const resolvedCommonDir = path15.isAbsolute(commonDir) ? commonDir : path15.resolve(repoPath, commonDir);
|
|
23182
|
+
return path15.join(resolvedCommonDir, "wtx", "stack.json");
|
|
23183
|
+
}
|
|
23184
|
+
async function readStackMetadata(repoPath, opts = { verbose: false, dryRun: false }) {
|
|
23185
|
+
const filePath = await metadataPath(repoPath, opts);
|
|
23186
|
+
if (!filePath || !fs7.existsSync(filePath))
|
|
23187
|
+
return emptyMetadata();
|
|
23188
|
+
try {
|
|
23189
|
+
const raw = JSON.parse(fs7.readFileSync(filePath, "utf8"));
|
|
23190
|
+
return parseMetadata(raw);
|
|
23191
|
+
} catch (err) {
|
|
23192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23193
|
+
throw new Error(`Failed to read stack metadata: ${message}`);
|
|
23194
|
+
}
|
|
23195
|
+
}
|
|
23196
|
+
async function writeStackMetadata(repoPath, metadata, opts) {
|
|
23197
|
+
if (opts.dryRun)
|
|
23198
|
+
return;
|
|
23199
|
+
const filePath = await metadataPath(repoPath, opts);
|
|
23200
|
+
if (!filePath)
|
|
23201
|
+
return;
|
|
23202
|
+
const dir = path15.dirname(filePath);
|
|
23203
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
23204
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
23205
|
+
fs7.writeFileSync(tempPath, `${JSON.stringify(metadata, null, 2)}
|
|
23206
|
+
`, "utf8");
|
|
23207
|
+
fs7.renameSync(tempPath, filePath);
|
|
23208
|
+
}
|
|
23209
|
+
async function recordStackEntry(repoPath, branch, entry, opts) {
|
|
23210
|
+
const metadata = await readStackMetadata(repoPath, opts);
|
|
23211
|
+
metadata.branches[branch] = entry;
|
|
23212
|
+
await writeStackMetadata(repoPath, metadata, opts);
|
|
23213
|
+
}
|
|
23214
|
+
async function removeStackEntry(repoPath, branch, opts) {
|
|
23215
|
+
const metadata = await readStackMetadata(repoPath, opts);
|
|
23216
|
+
if (!metadata.branches[branch])
|
|
23217
|
+
return;
|
|
23218
|
+
delete metadata.branches[branch];
|
|
23219
|
+
await writeStackMetadata(repoPath, metadata, opts);
|
|
23220
|
+
}
|
|
23221
|
+
async function renameStackEntry(repoPath, oldBranch, newBranch, opts) {
|
|
23222
|
+
const metadata = await readStackMetadata(repoPath, opts);
|
|
23223
|
+
const entry = metadata.branches[oldBranch];
|
|
23224
|
+
let changed = false;
|
|
23225
|
+
if (entry) {
|
|
23226
|
+
metadata.branches[newBranch] = entry;
|
|
23227
|
+
delete metadata.branches[oldBranch];
|
|
23228
|
+
changed = true;
|
|
23229
|
+
}
|
|
23230
|
+
for (const child of Object.values(metadata.branches)) {
|
|
23231
|
+
if (child.baseRef !== oldBranch && child.baseRef !== `refs/heads/${oldBranch}`)
|
|
23232
|
+
continue;
|
|
23233
|
+
child.baseRef = newBranch;
|
|
23234
|
+
changed = true;
|
|
23235
|
+
}
|
|
23236
|
+
if (changed)
|
|
23237
|
+
await writeStackMetadata(repoPath, metadata, opts);
|
|
23238
|
+
}
|
|
23239
|
+
function getStackChildren(metadata, branch) {
|
|
23240
|
+
return Object.entries(metadata.branches).filter(([, entry]) => entry.baseRef === branch || entry.baseRef === `refs/heads/${branch}`).map(([child]) => child).sort((a2, b) => a2.localeCompare(b));
|
|
23241
|
+
}
|
|
23242
|
+
function getStackAncestors(metadata, branch) {
|
|
23243
|
+
const ancestors = [branch];
|
|
23244
|
+
const seen = new Set([branch]);
|
|
23245
|
+
let current = branch;
|
|
23246
|
+
while (true) {
|
|
23247
|
+
const entry = metadata.branches[current];
|
|
23248
|
+
if (!entry || seen.has(entry.baseRef))
|
|
23249
|
+
break;
|
|
23250
|
+
ancestors.unshift(entry.baseRef);
|
|
23251
|
+
seen.add(entry.baseRef);
|
|
23252
|
+
current = entry.baseRef;
|
|
23253
|
+
}
|
|
23254
|
+
return ancestors;
|
|
23255
|
+
}
|
|
23256
|
+
function resolveParentBranch(baseRef, branches) {
|
|
23257
|
+
if (!baseRef)
|
|
23258
|
+
return null;
|
|
23259
|
+
if (branches.has(baseRef))
|
|
23260
|
+
return baseRef;
|
|
23261
|
+
const headRef = baseRef.replace(/^refs\/heads\//, "");
|
|
23262
|
+
if (branches.has(headRef))
|
|
23263
|
+
return headRef;
|
|
23264
|
+
const remoteRef = baseRef.replace(/^refs\/remotes\/[^/]+\//, "");
|
|
23265
|
+
if (branches.has(remoteRef))
|
|
23266
|
+
return remoteRef;
|
|
23267
|
+
const slash = baseRef.indexOf("/");
|
|
23268
|
+
if (slash > 0) {
|
|
23269
|
+
const shortRef = baseRef.substring(slash + 1);
|
|
23270
|
+
if (branches.has(shortRef))
|
|
23271
|
+
return shortRef;
|
|
23272
|
+
}
|
|
23273
|
+
return null;
|
|
23274
|
+
}
|
|
23275
|
+
function buildStackHierarchy(items, getBranch, getBase, compare) {
|
|
23276
|
+
const branchNames = new Set(items.map(getBranch).filter((branch) => branch !== undefined));
|
|
23277
|
+
const byBranch = new Map;
|
|
23278
|
+
const children = new Map;
|
|
23279
|
+
const roots = [];
|
|
23280
|
+
for (const item of items) {
|
|
23281
|
+
const branch = getBranch(item);
|
|
23282
|
+
if (branch)
|
|
23283
|
+
byBranch.set(branch, item);
|
|
23284
|
+
}
|
|
23285
|
+
for (const item of items) {
|
|
23286
|
+
const branch = getBranch(item);
|
|
23287
|
+
const parent = resolveParentBranch(getBase(item), branchNames);
|
|
23288
|
+
if (!parent || parent === branch || !byBranch.has(parent)) {
|
|
23289
|
+
roots.push(item);
|
|
23290
|
+
continue;
|
|
23291
|
+
}
|
|
23292
|
+
const siblings = children.get(parent) ?? [];
|
|
23293
|
+
siblings.push(item);
|
|
23294
|
+
children.set(parent, siblings);
|
|
23295
|
+
}
|
|
23296
|
+
const result = [];
|
|
23297
|
+
const visited = new Set;
|
|
23298
|
+
const visit = (item, depth, ancestorPrefix, isLast) => {
|
|
23299
|
+
const branch = getBranch(item);
|
|
23300
|
+
if (branch && visited.has(branch))
|
|
23301
|
+
return;
|
|
23302
|
+
if (branch)
|
|
23303
|
+
visited.add(branch);
|
|
23304
|
+
const prefix = depth === 0 ? "" : `${ancestorPrefix}${isLast ? "└─ " : "├─ "}`;
|
|
23305
|
+
result.push({ item, depth, prefix });
|
|
23306
|
+
const childRows = [...branch ? children.get(branch) ?? [] : []].sort(compare);
|
|
23307
|
+
const childPrefix = depth === 0 ? "" : `${ancestorPrefix}${isLast ? " " : "│ "}`;
|
|
23308
|
+
childRows.forEach((child, index) => {
|
|
23309
|
+
visit(child, depth + 1, childPrefix, index === childRows.length - 1);
|
|
23310
|
+
});
|
|
23311
|
+
};
|
|
23312
|
+
roots.sort(compare).forEach((root) => visit(root, 0, "", true));
|
|
23313
|
+
for (const item of items) {
|
|
23314
|
+
const branch = getBranch(item);
|
|
23315
|
+
if (!branch || !visited.has(branch))
|
|
23316
|
+
visit(item, 0, "", true);
|
|
23317
|
+
}
|
|
23318
|
+
return result;
|
|
23319
|
+
}
|
|
23320
|
+
var init_stack = __esm(() => {
|
|
23321
|
+
init_git();
|
|
23322
|
+
});
|
|
23323
|
+
|
|
23324
|
+
// src/lib/resolver.ts
|
|
23325
|
+
import fs8 from "fs";
|
|
23326
|
+
import path16 from "path";
|
|
23137
23327
|
function detectRepoFromCwd(config2) {
|
|
23138
23328
|
const cwd = process.cwd();
|
|
23139
23329
|
const root = expandTilde(config2.root);
|
|
23140
23330
|
for (const name of Object.keys(config2.repos)) {
|
|
23141
|
-
const mainPath =
|
|
23142
|
-
const wtRoot =
|
|
23143
|
-
if (cwd === mainPath || cwd.startsWith(mainPath +
|
|
23331
|
+
const mainPath = path16.join(root, name);
|
|
23332
|
+
const wtRoot = path16.join(root, `${name}${config2.postfix}`);
|
|
23333
|
+
if (cwd === mainPath || cwd.startsWith(mainPath + path16.sep)) {
|
|
23144
23334
|
return name;
|
|
23145
23335
|
}
|
|
23146
|
-
if (cwd === wtRoot || cwd.startsWith(wtRoot +
|
|
23336
|
+
if (cwd === wtRoot || cwd.startsWith(wtRoot + path16.sep)) {
|
|
23147
23337
|
return name;
|
|
23148
23338
|
}
|
|
23149
23339
|
}
|
|
@@ -23173,11 +23363,11 @@ function resolveRepos(config2, repoFilter) {
|
|
|
23173
23363
|
return targetRepos.map((name) => {
|
|
23174
23364
|
const mainPath = `${expandTilde(config2.root)}/${name}`;
|
|
23175
23365
|
const wtRoot = `${expandTilde(config2.root)}/${name}${config2.postfix}`;
|
|
23176
|
-
if (!
|
|
23366
|
+
if (!fs8.existsSync(mainPath)) {
|
|
23177
23367
|
throw new Error(`Repo directory not found at ${mainPath}`);
|
|
23178
23368
|
}
|
|
23179
|
-
const gitDir =
|
|
23180
|
-
if (!
|
|
23369
|
+
const gitDir = path16.join(mainPath, ".git");
|
|
23370
|
+
if (!fs8.existsSync(gitDir)) {
|
|
23181
23371
|
throw new Error(`Not a git repository: ${mainPath}`);
|
|
23182
23372
|
}
|
|
23183
23373
|
return {
|
|
@@ -23240,14 +23430,14 @@ function expandTemplate(template, vars) {
|
|
|
23240
23430
|
}
|
|
23241
23431
|
|
|
23242
23432
|
// src/lib/deps/workspaces.ts
|
|
23243
|
-
import
|
|
23244
|
-
import
|
|
23433
|
+
import fs11 from "fs";
|
|
23434
|
+
import path18 from "path";
|
|
23245
23435
|
function getWorkspaceDirs(rootDir) {
|
|
23246
23436
|
const dirs = new Set;
|
|
23247
|
-
const pkgPath =
|
|
23248
|
-
if (
|
|
23437
|
+
const pkgPath = path18.join(rootDir, "package.json");
|
|
23438
|
+
if (fs11.existsSync(pkgPath)) {
|
|
23249
23439
|
try {
|
|
23250
|
-
const pkg = JSON.parse(
|
|
23440
|
+
const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
|
|
23251
23441
|
if (pkg.workspaces) {
|
|
23252
23442
|
let ws = [];
|
|
23253
23443
|
if (Array.isArray(pkg.workspaces)) {
|
|
@@ -23261,10 +23451,10 @@ function getWorkspaceDirs(rootDir) {
|
|
|
23261
23451
|
}
|
|
23262
23452
|
} catch {}
|
|
23263
23453
|
}
|
|
23264
|
-
const pnpmPath =
|
|
23265
|
-
if (
|
|
23454
|
+
const pnpmPath = path18.join(rootDir, "pnpm-workspace.yaml");
|
|
23455
|
+
if (fs11.existsSync(pnpmPath)) {
|
|
23266
23456
|
try {
|
|
23267
|
-
const content =
|
|
23457
|
+
const content = fs11.readFileSync(pnpmPath, "utf-8");
|
|
23268
23458
|
const lines = content.split(/\r?\n/);
|
|
23269
23459
|
let inPackages = false;
|
|
23270
23460
|
for (let line of lines) {
|
|
@@ -23295,8 +23485,8 @@ function resolvePattern(rootDir, pattern, out) {
|
|
|
23295
23485
|
const normalized = pattern.replace(/\/+$/, "");
|
|
23296
23486
|
const hasGlob = normalized.includes("*");
|
|
23297
23487
|
if (!hasGlob) {
|
|
23298
|
-
const fullDir =
|
|
23299
|
-
if (
|
|
23488
|
+
const fullDir = path18.join(rootDir, normalized);
|
|
23489
|
+
if (fs11.existsSync(fullDir) && fs11.statSync(fullDir).isDirectory()) {
|
|
23300
23490
|
out.add(normalized);
|
|
23301
23491
|
}
|
|
23302
23492
|
return;
|
|
@@ -23311,12 +23501,12 @@ function globToRegex(pattern) {
|
|
|
23311
23501
|
function collectMatchingDirs(base2, rel, regex, out, depth) {
|
|
23312
23502
|
if (depth > 8)
|
|
23313
23503
|
return;
|
|
23314
|
-
if (rel && regex.test(rel) &&
|
|
23504
|
+
if (rel && regex.test(rel) && fs11.existsSync(path18.join(base2, rel, "package.json"))) {
|
|
23315
23505
|
out.add(rel);
|
|
23316
23506
|
}
|
|
23317
23507
|
let entries;
|
|
23318
23508
|
try {
|
|
23319
|
-
entries =
|
|
23509
|
+
entries = fs11.readdirSync(rel ? path18.join(base2, rel) : base2, { withFileTypes: true });
|
|
23320
23510
|
} catch {
|
|
23321
23511
|
return;
|
|
23322
23512
|
}
|
|
@@ -23324,7 +23514,7 @@ function collectMatchingDirs(base2, rel, regex, out, depth) {
|
|
|
23324
23514
|
if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) {
|
|
23325
23515
|
continue;
|
|
23326
23516
|
}
|
|
23327
|
-
const nextRel = rel ?
|
|
23517
|
+
const nextRel = rel ? path18.posix.join(rel, entry.name) : entry.name;
|
|
23328
23518
|
if (!regex.test(nextRel) && !couldMatchDeeper(regex.source, nextRel))
|
|
23329
23519
|
continue;
|
|
23330
23520
|
collectMatchingDirs(base2, nextRel, regex, out, depth + 1);
|
|
@@ -23336,21 +23526,21 @@ function couldMatchDeeper(regexSource, rel) {
|
|
|
23336
23526
|
var init_workspaces = () => {};
|
|
23337
23527
|
|
|
23338
23528
|
// src/lib/deps/diff.ts
|
|
23339
|
-
import
|
|
23340
|
-
import
|
|
23529
|
+
import fs12 from "fs";
|
|
23530
|
+
import path19 from "path";
|
|
23341
23531
|
function filesMatch(wtPath, mainPath, fileNames) {
|
|
23342
23532
|
for (const name of fileNames) {
|
|
23343
|
-
const wtFile =
|
|
23344
|
-
const mainFile =
|
|
23345
|
-
const wtExists =
|
|
23346
|
-
const mainExists =
|
|
23533
|
+
const wtFile = path19.join(wtPath, name);
|
|
23534
|
+
const mainFile = path19.join(mainPath, name);
|
|
23535
|
+
const wtExists = fs12.existsSync(wtFile);
|
|
23536
|
+
const mainExists = fs12.existsSync(mainFile);
|
|
23347
23537
|
if (wtExists !== mainExists)
|
|
23348
23538
|
return false;
|
|
23349
23539
|
if (!wtExists)
|
|
23350
23540
|
continue;
|
|
23351
23541
|
try {
|
|
23352
|
-
const wtContent =
|
|
23353
|
-
const mainContent =
|
|
23542
|
+
const wtContent = fs12.readFileSync(wtFile);
|
|
23543
|
+
const mainContent = fs12.readFileSync(mainFile);
|
|
23354
23544
|
if (!wtContent.equals(mainContent))
|
|
23355
23545
|
return false;
|
|
23356
23546
|
} catch {
|
|
@@ -23368,7 +23558,7 @@ function getWorkspaceDelta(wtPath, mainPath, lockfileNames) {
|
|
|
23368
23558
|
const changedWorkspaces = [];
|
|
23369
23559
|
for (const ws of allWorkspaces) {
|
|
23370
23560
|
const wsFiles = ["package.json"];
|
|
23371
|
-
if (!filesMatch(
|
|
23561
|
+
if (!filesMatch(path19.join(wtPath, ws), path19.join(mainPath, ws), wsFiles)) {
|
|
23372
23562
|
changedWorkspaces.push(ws);
|
|
23373
23563
|
}
|
|
23374
23564
|
}
|
|
@@ -23379,8 +23569,8 @@ var init_diff = __esm(() => {
|
|
|
23379
23569
|
});
|
|
23380
23570
|
|
|
23381
23571
|
// src/lib/deps/adapters/bun.ts
|
|
23382
|
-
import
|
|
23383
|
-
import
|
|
23572
|
+
import fs13 from "fs";
|
|
23573
|
+
import path20 from "path";
|
|
23384
23574
|
var bunAdapter;
|
|
23385
23575
|
var init_bun = __esm(() => {
|
|
23386
23576
|
init_diff();
|
|
@@ -23388,7 +23578,7 @@ var init_bun = __esm(() => {
|
|
|
23388
23578
|
bunAdapter = {
|
|
23389
23579
|
id: "bun",
|
|
23390
23580
|
displayName: "bun",
|
|
23391
|
-
detect: (dir) =>
|
|
23581
|
+
detect: (dir) => fs13.existsSync(path20.join(dir, "bun.lockb")) || fs13.existsSync(path20.join(dir, "bun.lock")),
|
|
23392
23582
|
lockfileNames: ["bun.lockb", "bun.lock"],
|
|
23393
23583
|
definitionsMatch: (wtPath, mainPath) => {
|
|
23394
23584
|
const delta = getWorkspaceDelta(wtPath, mainPath, ["bun.lockb", "bun.lock"]);
|
|
@@ -23416,8 +23606,8 @@ var init_bun = __esm(() => {
|
|
|
23416
23606
|
});
|
|
23417
23607
|
|
|
23418
23608
|
// src/lib/deps/adapters/pnpm.ts
|
|
23419
|
-
import
|
|
23420
|
-
import
|
|
23609
|
+
import fs14 from "fs";
|
|
23610
|
+
import path21 from "path";
|
|
23421
23611
|
var pnpmAdapter;
|
|
23422
23612
|
var init_pnpm = __esm(() => {
|
|
23423
23613
|
init_diff();
|
|
@@ -23425,7 +23615,7 @@ var init_pnpm = __esm(() => {
|
|
|
23425
23615
|
pnpmAdapter = {
|
|
23426
23616
|
id: "pnpm",
|
|
23427
23617
|
displayName: "pnpm",
|
|
23428
|
-
detect: (dir) =>
|
|
23618
|
+
detect: (dir) => fs14.existsSync(path21.join(dir, "pnpm-lock.yaml")),
|
|
23429
23619
|
lockfileNames: ["pnpm-lock.yaml"],
|
|
23430
23620
|
definitionsMatch: (wtPath, mainPath) => {
|
|
23431
23621
|
const delta = getWorkspaceDelta(wtPath, mainPath, ["pnpm-lock.yaml"]);
|
|
@@ -23453,8 +23643,8 @@ var init_pnpm = __esm(() => {
|
|
|
23453
23643
|
});
|
|
23454
23644
|
|
|
23455
23645
|
// src/lib/deps/adapters/yarn.ts
|
|
23456
|
-
import
|
|
23457
|
-
import
|
|
23646
|
+
import fs15 from "fs";
|
|
23647
|
+
import path22 from "path";
|
|
23458
23648
|
var yarnAdapter;
|
|
23459
23649
|
var init_yarn = __esm(() => {
|
|
23460
23650
|
init_diff();
|
|
@@ -23462,7 +23652,7 @@ var init_yarn = __esm(() => {
|
|
|
23462
23652
|
yarnAdapter = {
|
|
23463
23653
|
id: "yarn",
|
|
23464
23654
|
displayName: "yarn",
|
|
23465
|
-
detect: (dir) =>
|
|
23655
|
+
detect: (dir) => fs15.existsSync(path22.join(dir, "yarn.lock")),
|
|
23466
23656
|
lockfileNames: ["yarn.lock"],
|
|
23467
23657
|
definitionsMatch: (wtPath, mainPath) => {
|
|
23468
23658
|
const delta = getWorkspaceDelta(wtPath, mainPath, ["yarn.lock"]);
|
|
@@ -23484,8 +23674,8 @@ var init_yarn = __esm(() => {
|
|
|
23484
23674
|
});
|
|
23485
23675
|
|
|
23486
23676
|
// src/lib/deps/adapters/npm.ts
|
|
23487
|
-
import
|
|
23488
|
-
import
|
|
23677
|
+
import fs16 from "fs";
|
|
23678
|
+
import path23 from "path";
|
|
23489
23679
|
var npmAdapter;
|
|
23490
23680
|
var init_npm = __esm(() => {
|
|
23491
23681
|
init_diff();
|
|
@@ -23493,7 +23683,7 @@ var init_npm = __esm(() => {
|
|
|
23493
23683
|
npmAdapter = {
|
|
23494
23684
|
id: "npm",
|
|
23495
23685
|
displayName: "npm",
|
|
23496
|
-
detect: (dir) =>
|
|
23686
|
+
detect: (dir) => fs16.existsSync(path23.join(dir, "package-lock.json")),
|
|
23497
23687
|
lockfileNames: ["package-lock.json"],
|
|
23498
23688
|
definitionsMatch: (wtPath, mainPath) => {
|
|
23499
23689
|
const delta = getWorkspaceDelta(wtPath, mainPath, ["package-lock.json"]);
|
|
@@ -23850,47 +24040,47 @@ var init_registry = __esm(() => {
|
|
|
23850
24040
|
});
|
|
23851
24041
|
|
|
23852
24042
|
// src/lib/deps/linking.ts
|
|
23853
|
-
import
|
|
23854
|
-
import
|
|
24043
|
+
import fs17 from "fs";
|
|
24044
|
+
import path24 from "path";
|
|
23855
24045
|
function performSafeLink(wtPath, mainPath, dryRun, quiet) {
|
|
23856
|
-
const wtNm =
|
|
23857
|
-
const mainNm =
|
|
23858
|
-
if (!
|
|
24046
|
+
const wtNm = path24.join(wtPath, "node_modules");
|
|
24047
|
+
const mainNm = path24.join(mainPath, "node_modules");
|
|
24048
|
+
if (!fs17.existsSync(mainNm)) {
|
|
23859
24049
|
return;
|
|
23860
24050
|
}
|
|
23861
24051
|
let existingLink = false;
|
|
23862
24052
|
try {
|
|
23863
|
-
const stat =
|
|
24053
|
+
const stat = fs17.lstatSync(wtNm);
|
|
23864
24054
|
existingLink = stat.isSymbolicLink();
|
|
23865
24055
|
} catch {}
|
|
23866
24056
|
if (existingLink) {
|
|
23867
24057
|
if (!quiet)
|
|
23868
24058
|
stepProgress("Removing whole-directory symlink to prepare for safe linking...");
|
|
23869
24059
|
if (!dryRun) {
|
|
23870
|
-
|
|
24060
|
+
fs17.unlinkSync(wtNm);
|
|
23871
24061
|
}
|
|
23872
24062
|
}
|
|
23873
|
-
if (!dryRun && !
|
|
23874
|
-
|
|
24063
|
+
if (!dryRun && !fs17.existsSync(wtNm)) {
|
|
24064
|
+
fs17.mkdirSync(wtNm, { recursive: true });
|
|
23875
24065
|
}
|
|
23876
24066
|
const entriesToLink = [];
|
|
23877
24067
|
try {
|
|
23878
|
-
const mainEntries =
|
|
24068
|
+
const mainEntries = fs17.readdirSync(mainNm, { withFileTypes: true });
|
|
23879
24069
|
for (const entry of mainEntries) {
|
|
23880
24070
|
if (entry.name === ".bin") {
|
|
23881
24071
|
entriesToLink.push({
|
|
23882
24072
|
name: ".bin",
|
|
23883
|
-
target:
|
|
24073
|
+
target: path24.join(mainNm, ".bin"),
|
|
23884
24074
|
isBin: true
|
|
23885
24075
|
});
|
|
23886
24076
|
} else if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
23887
|
-
const scopePath =
|
|
24077
|
+
const scopePath = path24.join(mainNm, entry.name);
|
|
23888
24078
|
try {
|
|
23889
|
-
const scopedEntries =
|
|
24079
|
+
const scopedEntries = fs17.readdirSync(scopePath, { withFileTypes: true });
|
|
23890
24080
|
for (const scopedEntry of scopedEntries) {
|
|
23891
24081
|
entriesToLink.push({
|
|
23892
|
-
name:
|
|
23893
|
-
target:
|
|
24082
|
+
name: path24.join(entry.name, scopedEntry.name),
|
|
24083
|
+
target: path24.join(scopePath, scopedEntry.name),
|
|
23894
24084
|
isBin: false
|
|
23895
24085
|
});
|
|
23896
24086
|
}
|
|
@@ -23898,7 +24088,7 @@ function performSafeLink(wtPath, mainPath, dryRun, quiet) {
|
|
|
23898
24088
|
} else {
|
|
23899
24089
|
entriesToLink.push({
|
|
23900
24090
|
name: entry.name,
|
|
23901
|
-
target:
|
|
24091
|
+
target: path24.join(mainNm, entry.name),
|
|
23902
24092
|
isBin: false
|
|
23903
24093
|
});
|
|
23904
24094
|
}
|
|
@@ -23908,7 +24098,7 @@ function performSafeLink(wtPath, mainPath, dryRun, quiet) {
|
|
|
23908
24098
|
let failedCount = 0;
|
|
23909
24099
|
const failedNames = [];
|
|
23910
24100
|
for (const { name, target, isBin } of entriesToLink) {
|
|
23911
|
-
const linkPath =
|
|
24101
|
+
const linkPath = path24.join(wtNm, name);
|
|
23912
24102
|
if (dryRun) {
|
|
23913
24103
|
if (!quiet)
|
|
23914
24104
|
info(` [dry-run] Would link ${name}`);
|
|
@@ -23916,41 +24106,41 @@ function performSafeLink(wtPath, mainPath, dryRun, quiet) {
|
|
|
23916
24106
|
continue;
|
|
23917
24107
|
}
|
|
23918
24108
|
try {
|
|
23919
|
-
const parentDir =
|
|
23920
|
-
if (!
|
|
23921
|
-
|
|
24109
|
+
const parentDir = path24.dirname(linkPath);
|
|
24110
|
+
if (!fs17.existsSync(parentDir)) {
|
|
24111
|
+
fs17.mkdirSync(parentDir, { recursive: true });
|
|
23922
24112
|
}
|
|
23923
24113
|
let shouldLink = true;
|
|
23924
24114
|
let backupPath = null;
|
|
23925
24115
|
try {
|
|
23926
|
-
const stat =
|
|
24116
|
+
const stat = fs17.lstatSync(linkPath);
|
|
23927
24117
|
if (stat.isSymbolicLink()) {
|
|
23928
|
-
const existingTarget =
|
|
23929
|
-
const resolvedExisting =
|
|
24118
|
+
const existingTarget = fs17.readlinkSync(linkPath);
|
|
24119
|
+
const resolvedExisting = path24.resolve(path24.dirname(linkPath), existingTarget);
|
|
23930
24120
|
if (resolvedExisting === target) {
|
|
23931
24121
|
shouldLink = false;
|
|
23932
24122
|
} else {
|
|
23933
|
-
|
|
24123
|
+
fs17.unlinkSync(linkPath);
|
|
23934
24124
|
}
|
|
23935
24125
|
} else {
|
|
23936
24126
|
backupPath = `${linkPath}.wtx-old`;
|
|
23937
|
-
|
|
24127
|
+
fs17.renameSync(linkPath, backupPath);
|
|
23938
24128
|
}
|
|
23939
24129
|
} catch {}
|
|
23940
24130
|
if (shouldLink) {
|
|
23941
24131
|
try {
|
|
23942
|
-
const relTarget =
|
|
23943
|
-
|
|
24132
|
+
const relTarget = path24.relative(path24.dirname(linkPath), target);
|
|
24133
|
+
fs17.symlinkSync(relTarget, linkPath, isBin ? "dir" : fs17.statSync(target).isDirectory() ? "dir" : "file");
|
|
23944
24134
|
createdCount++;
|
|
23945
24135
|
} catch (err) {
|
|
23946
24136
|
if (backupPath) {
|
|
23947
|
-
|
|
24137
|
+
fs17.renameSync(backupPath, linkPath);
|
|
23948
24138
|
}
|
|
23949
24139
|
throw err;
|
|
23950
24140
|
}
|
|
23951
24141
|
}
|
|
23952
|
-
if (backupPath &&
|
|
23953
|
-
|
|
24142
|
+
if (backupPath && fs17.existsSync(backupPath)) {
|
|
24143
|
+
fs17.rmSync(backupPath, { recursive: true, force: true });
|
|
23954
24144
|
}
|
|
23955
24145
|
} catch {
|
|
23956
24146
|
failedCount++;
|
|
@@ -23969,15 +24159,15 @@ var init_linking = __esm(() => {
|
|
|
23969
24159
|
});
|
|
23970
24160
|
|
|
23971
24161
|
// src/lib/deps/engine.ts
|
|
23972
|
-
import
|
|
23973
|
-
import
|
|
24162
|
+
import fs18 from "fs";
|
|
24163
|
+
import path25 from "path";
|
|
23974
24164
|
function resolveAdapter2(dir, managerOverride) {
|
|
23975
24165
|
return resolveAdapter(dir, managerOverride);
|
|
23976
24166
|
}
|
|
23977
24167
|
function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
|
|
23978
|
-
const nmPath =
|
|
23979
|
-
const mainNm =
|
|
23980
|
-
if (!
|
|
24168
|
+
const nmPath = path25.join(wtPath, "node_modules");
|
|
24169
|
+
const mainNm = path25.join(mainPath, "node_modules");
|
|
24170
|
+
if (!fs18.existsSync(mainNm)) {
|
|
23981
24171
|
if (!quiet)
|
|
23982
24172
|
stepWarning("Main repo has no node_modules to symlink to");
|
|
23983
24173
|
return;
|
|
@@ -23987,7 +24177,7 @@ function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
|
|
|
23987
24177
|
let existingLink = false;
|
|
23988
24178
|
let shouldRemove = false;
|
|
23989
24179
|
try {
|
|
23990
|
-
const stat =
|
|
24180
|
+
const stat = fs18.lstatSync(nmPath);
|
|
23991
24181
|
existingLink = stat.isSymbolicLink();
|
|
23992
24182
|
shouldRemove = true;
|
|
23993
24183
|
} catch {}
|
|
@@ -24001,9 +24191,9 @@ function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
|
|
|
24001
24191
|
}
|
|
24002
24192
|
if (!dryRun) {
|
|
24003
24193
|
if (existingLink) {
|
|
24004
|
-
|
|
24194
|
+
fs18.unlinkSync(nmPath);
|
|
24005
24195
|
} else {
|
|
24006
|
-
|
|
24196
|
+
fs18.rmSync(nmPath, { recursive: true, force: true });
|
|
24007
24197
|
}
|
|
24008
24198
|
}
|
|
24009
24199
|
}
|
|
@@ -24011,22 +24201,22 @@ function performLegacySymlink(wtPath, mainPath, dryRun, quiet) {
|
|
|
24011
24201
|
if (!quiet)
|
|
24012
24202
|
info(` [dry-run] Would symlink ${mainNm} to ${nmPath}`);
|
|
24013
24203
|
} else {
|
|
24014
|
-
|
|
24204
|
+
fs18.symlinkSync(mainNm, nmPath);
|
|
24015
24205
|
}
|
|
24016
24206
|
if (!quiet)
|
|
24017
24207
|
stepSuccess("Symlinked node_modules", mainNm);
|
|
24018
24208
|
}
|
|
24019
24209
|
function detectCommonLinkageState(wtPath, mainPath) {
|
|
24020
|
-
const nodeModulesPath =
|
|
24210
|
+
const nodeModulesPath = path25.join(wtPath, "node_modules");
|
|
24021
24211
|
try {
|
|
24022
|
-
const stat =
|
|
24212
|
+
const stat = fs18.lstatSync(nodeModulesPath);
|
|
24023
24213
|
if (stat.isSymbolicLink()) {
|
|
24024
|
-
const target =
|
|
24025
|
-
const resolvedTarget =
|
|
24214
|
+
const target = fs18.readlinkSync(nodeModulesPath);
|
|
24215
|
+
const resolvedTarget = path25.resolve(wtPath, target);
|
|
24026
24216
|
const resolvedMain = safeResolve(mainPath);
|
|
24027
24217
|
let targetExists = false;
|
|
24028
24218
|
try {
|
|
24029
|
-
|
|
24219
|
+
fs18.statSync(resolvedTarget);
|
|
24030
24220
|
targetExists = true;
|
|
24031
24221
|
} catch {
|
|
24032
24222
|
targetExists = false;
|
|
@@ -24034,14 +24224,14 @@ function detectCommonLinkageState(wtPath, mainPath) {
|
|
|
24034
24224
|
if (!targetExists) {
|
|
24035
24225
|
return { state: "broken", target };
|
|
24036
24226
|
}
|
|
24037
|
-
if (!isWithin(resolvedMain, resolvedTarget) && resolvedTarget !==
|
|
24227
|
+
if (!isWithin(resolvedMain, resolvedTarget) && resolvedTarget !== path25.join(resolvedMain, "node_modules")) {
|
|
24038
24228
|
return { state: "external", target };
|
|
24039
24229
|
}
|
|
24040
24230
|
return { state: "linked-whole", target };
|
|
24041
24231
|
} else if (stat.isDirectory()) {
|
|
24042
24232
|
let isLinkedPackages = false;
|
|
24043
24233
|
try {
|
|
24044
|
-
const entries =
|
|
24234
|
+
const entries = fs18.readdirSync(nodeModulesPath, { withFileTypes: true });
|
|
24045
24235
|
for (const entry of entries) {
|
|
24046
24236
|
if (entry.isSymbolicLink()) {
|
|
24047
24237
|
isLinkedPackages = true;
|
|
@@ -24055,7 +24245,7 @@ function detectCommonLinkageState(wtPath, mainPath) {
|
|
|
24055
24245
|
return { state: "installed" };
|
|
24056
24246
|
}
|
|
24057
24247
|
} catch {
|
|
24058
|
-
if (!
|
|
24248
|
+
if (!fs18.existsSync(nodeModulesPath)) {
|
|
24059
24249
|
return { state: "missing" };
|
|
24060
24250
|
}
|
|
24061
24251
|
}
|
|
@@ -24169,7 +24359,7 @@ function findRepoDepsContext(wtPath) {
|
|
|
24169
24359
|
try {
|
|
24170
24360
|
const config2 = loadConfig();
|
|
24171
24361
|
const repos = resolveRepos(config2, []);
|
|
24172
|
-
const repo = repos.find((r) => wtPath === r.wtRoot || wtPath.startsWith(r.wtRoot + "/"));
|
|
24362
|
+
const repo = repos.find((r) => wtPath === r.mainPath || wtPath === r.wtRoot || wtPath.startsWith(r.wtRoot + "/"));
|
|
24173
24363
|
if (repo) {
|
|
24174
24364
|
return {
|
|
24175
24365
|
name: repo.name,
|
|
@@ -24250,18 +24440,18 @@ var init_deps = __esm(() => {
|
|
|
24250
24440
|
});
|
|
24251
24441
|
|
|
24252
24442
|
// src/lib/forge/map.ts
|
|
24253
|
-
function
|
|
24443
|
+
function isRecord2(value) {
|
|
24254
24444
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24255
24445
|
}
|
|
24256
24446
|
function asCheckItem(value) {
|
|
24257
|
-
if (!
|
|
24447
|
+
if (!isRecord2(value))
|
|
24258
24448
|
return null;
|
|
24259
24449
|
return {
|
|
24260
24450
|
__typename: typeof value.__typename === "string" ? value.__typename : undefined,
|
|
24261
24451
|
status: typeof value.status === "string" ? value.status : null,
|
|
24262
24452
|
conclusion: typeof value.conclusion === "string" ? value.conclusion : null,
|
|
24263
24453
|
state: typeof value.state === "string" ? value.state : null,
|
|
24264
|
-
commit:
|
|
24454
|
+
commit: isRecord2(value.commit) ? value.commit : null
|
|
24265
24455
|
};
|
|
24266
24456
|
}
|
|
24267
24457
|
function collectCheckItems(raw) {
|
|
@@ -24335,7 +24525,7 @@ function mapReviewDecision(raw) {
|
|
|
24335
24525
|
return null;
|
|
24336
24526
|
}
|
|
24337
24527
|
function mapGithubPr(raw) {
|
|
24338
|
-
if (!
|
|
24528
|
+
if (!isRecord2(raw))
|
|
24339
24529
|
return null;
|
|
24340
24530
|
const number4 = typeof raw.number === "number" ? raw.number : null;
|
|
24341
24531
|
const headRefName = typeof raw.headRefName === "string" ? raw.headRefName : null;
|
|
@@ -24346,9 +24536,10 @@ function mapGithubPr(raw) {
|
|
|
24346
24536
|
const url2 = typeof raw.url === "string" ? raw.url : "";
|
|
24347
24537
|
const isDraft = raw.isDraft === true;
|
|
24348
24538
|
const updatedAt = typeof raw.updatedAt === "string" ? raw.updatedAt : "";
|
|
24539
|
+
const baseRefName = typeof raw.baseRefName === "string" ? raw.baseRefName : undefined;
|
|
24349
24540
|
const checks3 = bucketChecks(collectCheckItems(raw.statusCheckRollup));
|
|
24350
24541
|
const author = raw.author;
|
|
24351
|
-
const authorLogin =
|
|
24542
|
+
const authorLogin = isRecord2(author) && typeof author.login === "string" ? author.login : null;
|
|
24352
24543
|
return {
|
|
24353
24544
|
number: number4,
|
|
24354
24545
|
authorLogin,
|
|
@@ -24359,12 +24550,13 @@ function mapGithubPr(raw) {
|
|
|
24359
24550
|
mergeable: mapMergeable(raw.mergeable),
|
|
24360
24551
|
checks: checks3,
|
|
24361
24552
|
reviewDecision: mapReviewDecision(raw.reviewDecision),
|
|
24553
|
+
...baseRefName ? { baseRefName } : {},
|
|
24362
24554
|
unresolvedThreads: 0,
|
|
24363
24555
|
updatedAt
|
|
24364
24556
|
};
|
|
24365
24557
|
}
|
|
24366
24558
|
function mapPrHead(raw) {
|
|
24367
|
-
if (!
|
|
24559
|
+
if (!isRecord2(raw))
|
|
24368
24560
|
return null;
|
|
24369
24561
|
const number4 = typeof raw.number === "number" ? raw.number : null;
|
|
24370
24562
|
const headRefName = typeof raw.headRefName === "string" ? raw.headRefName : null;
|
|
@@ -24375,12 +24567,13 @@ function mapPrHead(raw) {
|
|
|
24375
24567
|
const url2 = typeof raw.url === "string" ? raw.url : "";
|
|
24376
24568
|
const isDraft = raw.isDraft === true;
|
|
24377
24569
|
const isCrossRepository = raw.isCrossRepository === true;
|
|
24570
|
+
const baseRefName = typeof raw.baseRefName === "string" ? raw.baseRefName : undefined;
|
|
24378
24571
|
let headOwnerLogin = null;
|
|
24379
|
-
if (
|
|
24572
|
+
if (isRecord2(raw.headRepositoryOwner) && typeof raw.headRepositoryOwner.login === "string") {
|
|
24380
24573
|
headOwnerLogin = raw.headRepositoryOwner.login;
|
|
24381
24574
|
}
|
|
24382
24575
|
let headRepoName = null;
|
|
24383
|
-
if (
|
|
24576
|
+
if (isRecord2(raw.headRepository) && typeof raw.headRepository.name === "string") {
|
|
24384
24577
|
headRepoName = raw.headRepository.name;
|
|
24385
24578
|
}
|
|
24386
24579
|
return {
|
|
@@ -24390,6 +24583,7 @@ function mapPrHead(raw) {
|
|
|
24390
24583
|
state,
|
|
24391
24584
|
isDraft,
|
|
24392
24585
|
headRefName,
|
|
24586
|
+
...baseRefName ? { baseRefName } : {},
|
|
24393
24587
|
isCrossRepository,
|
|
24394
24588
|
headOwnerLogin,
|
|
24395
24589
|
headRepoName
|
|
@@ -24485,7 +24679,7 @@ async function ghExec(args, opts = {}) {
|
|
|
24485
24679
|
}
|
|
24486
24680
|
function mapWithBranch(raw) {
|
|
24487
24681
|
const pr = mapGithubPr(raw);
|
|
24488
|
-
if (!pr || !
|
|
24682
|
+
if (!pr || !isRecord2(raw) || typeof raw.headRefName !== "string")
|
|
24489
24683
|
return null;
|
|
24490
24684
|
return { branch: raw.headRefName, pr };
|
|
24491
24685
|
}
|
|
@@ -24530,12 +24724,12 @@ async function fetchBranchPrs(ctx) {
|
|
|
24530
24724
|
return results.filter((entry) => entry !== null);
|
|
24531
24725
|
}
|
|
24532
24726
|
function countUnresolvedThreads(result) {
|
|
24533
|
-
if (!
|
|
24727
|
+
if (!isRecord2(result))
|
|
24534
24728
|
return 0;
|
|
24535
24729
|
const threads = result.reviewThreads;
|
|
24536
|
-
if (!
|
|
24730
|
+
if (!isRecord2(threads) || !Array.isArray(threads.nodes))
|
|
24537
24731
|
return 0;
|
|
24538
|
-
return threads.nodes.filter((node) =>
|
|
24732
|
+
return threads.nodes.filter((node) => isRecord2(node) && node.isResolved === false).length;
|
|
24539
24733
|
}
|
|
24540
24734
|
function buildThreadQuery(numbers) {
|
|
24541
24735
|
const aliases = numbers.map((n2, i2) => `pr${i2}: pullRequest(number: ${n2}) { reviewThreads(first: ${THREADS_PER_PR}) { nodes { isResolved } } }`).join(" ");
|
|
@@ -24556,10 +24750,10 @@ async function enrichUnresolvedThreads(slug, prs, verboseFlag) {
|
|
|
24556
24750
|
`name=${slug.name}`
|
|
24557
24751
|
], { verbose: verboseFlag });
|
|
24558
24752
|
const payload = parseJson(stdout);
|
|
24559
|
-
if (!
|
|
24753
|
+
if (!isRecord2(payload) || !isRecord2(payload.data))
|
|
24560
24754
|
return;
|
|
24561
24755
|
const repoData = payload.data.repository;
|
|
24562
|
-
if (!
|
|
24756
|
+
if (!isRecord2(repoData))
|
|
24563
24757
|
return;
|
|
24564
24758
|
prs.forEach((pr, i2) => {
|
|
24565
24759
|
pr.unresolvedThreads = countUnresolvedThreads(repoData[`pr${i2}`]);
|
|
@@ -24589,7 +24783,7 @@ function createGithubAdapter(slug) {
|
|
|
24589
24783
|
"-R",
|
|
24590
24784
|
`${slug.owner}/${slug.name}`,
|
|
24591
24785
|
"--json",
|
|
24592
|
-
"number,title,url,state,isDraft,headRefName,isCrossRepository,headRepositoryOwner,headRepository"
|
|
24786
|
+
"number,title,url,state,isDraft,headRefName,baseRefName,isCrossRepository,headRepositoryOwner,headRepository"
|
|
24593
24787
|
];
|
|
24594
24788
|
const stdout = await ghExec(args, opts);
|
|
24595
24789
|
const parsed = parseJson(stdout);
|
|
@@ -24629,6 +24823,7 @@ var init_github = __esm(() => {
|
|
|
24629
24823
|
"statusCheckRollup",
|
|
24630
24824
|
"reviewDecision",
|
|
24631
24825
|
"headRefName",
|
|
24826
|
+
"baseRefName",
|
|
24632
24827
|
"updatedAt"
|
|
24633
24828
|
].join(",");
|
|
24634
24829
|
REMOTE_URL_PATTERNS = [
|
|
@@ -24639,8 +24834,8 @@ var init_github = __esm(() => {
|
|
|
24639
24834
|
});
|
|
24640
24835
|
|
|
24641
24836
|
// src/lib/forge/index.ts
|
|
24642
|
-
import
|
|
24643
|
-
import
|
|
24837
|
+
import fs19 from "fs";
|
|
24838
|
+
import path26 from "path";
|
|
24644
24839
|
function parsePrLink(link) {
|
|
24645
24840
|
for (const forge of FORGES) {
|
|
24646
24841
|
const ref = forge.parsePrLink(link);
|
|
@@ -24653,10 +24848,10 @@ function descriptorFor(id) {
|
|
|
24653
24848
|
return FORGES.find((f) => f.id === id) ?? null;
|
|
24654
24849
|
}
|
|
24655
24850
|
function readOriginUrl(mainPath) {
|
|
24656
|
-
const gitConfigPath =
|
|
24657
|
-
if (!
|
|
24851
|
+
const gitConfigPath = path26.join(mainPath, ".git", "config");
|
|
24852
|
+
if (!fs19.existsSync(gitConfigPath))
|
|
24658
24853
|
return null;
|
|
24659
|
-
const content =
|
|
24854
|
+
const content = fs19.readFileSync(gitConfigPath, "utf-8");
|
|
24660
24855
|
const originSection = content.match(/\[remote "origin"\]([^[]*)/);
|
|
24661
24856
|
if (!originSection)
|
|
24662
24857
|
return null;
|
|
@@ -24706,7 +24901,7 @@ var init_forge = __esm(() => {
|
|
|
24706
24901
|
});
|
|
24707
24902
|
|
|
24708
24903
|
// src/lib/owner.ts
|
|
24709
|
-
import
|
|
24904
|
+
import fs20 from "fs";
|
|
24710
24905
|
function deriveOwnership(input) {
|
|
24711
24906
|
if (input.hasLocalChanges) {
|
|
24712
24907
|
return { mine: true, author: null };
|
|
@@ -24733,7 +24928,7 @@ async function resolveOwnership(input) {
|
|
|
24733
24928
|
}
|
|
24734
24929
|
const { mainPath, branch, verbose: verbose2 } = input;
|
|
24735
24930
|
let hasLocalChanges = false;
|
|
24736
|
-
if (input.wtPath &&
|
|
24931
|
+
if (input.wtPath && fs20.existsSync(input.wtPath)) {
|
|
24737
24932
|
try {
|
|
24738
24933
|
const dirtyFiles = await getDirtyFiles(input.wtPath);
|
|
24739
24934
|
hasLocalChanges = dirtyFiles.length > 0;
|
|
@@ -24995,6 +25190,13 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
24995
25190
|
try {
|
|
24996
25191
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
24997
25192
|
const wts = await getWorktreeList(repo.mainPath);
|
|
25193
|
+
let stackMetadata = { version: 1, branches: {} };
|
|
25194
|
+
try {
|
|
25195
|
+
stackMetadata = await readStackMetadata(repo.mainPath, opts);
|
|
25196
|
+
} catch (err) {
|
|
25197
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
25198
|
+
warnings.push({ repoName: repo.name, message });
|
|
25199
|
+
}
|
|
24998
25200
|
const forge = resolveForge(repo);
|
|
24999
25201
|
const branches = wts.map((w) => w.branch).filter(Boolean);
|
|
25000
25202
|
let prMap = new Map;
|
|
@@ -25022,6 +25224,10 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
25022
25224
|
if (isMainCheckout && !branch) {
|
|
25023
25225
|
branch = mainBranch;
|
|
25024
25226
|
}
|
|
25227
|
+
const stackEntry = branch ? stackMetadata.branches[branch] : undefined;
|
|
25228
|
+
const pr = branch ? prMap.get(branch) : undefined;
|
|
25229
|
+
const base2 = stackEntry?.baseRef ?? pr?.baseRefName;
|
|
25230
|
+
const comparisonBase = stackEntry?.explicit ? stackEntry.baseRef : pr?.baseRefName && pr.baseRefName !== mainBranch ? pr.baseRefName : `origin/${mainBranch}`;
|
|
25025
25231
|
const row = {
|
|
25026
25232
|
repoName: repo.name,
|
|
25027
25233
|
branch: branch || "(detached)",
|
|
@@ -25040,7 +25246,9 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
25040
25246
|
prUrl: null,
|
|
25041
25247
|
owner: null,
|
|
25042
25248
|
rebaseStatus: null,
|
|
25043
|
-
depsStrategy: "none"
|
|
25249
|
+
depsStrategy: "none",
|
|
25250
|
+
base: base2,
|
|
25251
|
+
baseChanged: false
|
|
25044
25252
|
};
|
|
25045
25253
|
if (isMainCheckout) {
|
|
25046
25254
|
allRows.push(row);
|
|
@@ -25052,7 +25260,7 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
25052
25260
|
})(),
|
|
25053
25261
|
(async () => {
|
|
25054
25262
|
try {
|
|
25055
|
-
const stdout = await gitExec(["-C", wt.path, "rev-list", "--left-right", "--count",
|
|
25263
|
+
const stdout = await gitExec(["-C", wt.path, "rev-list", "--left-right", "--count", `${comparisonBase}...HEAD`], { dryRun: opts.dryRun });
|
|
25056
25264
|
if (stdout) {
|
|
25057
25265
|
const parts = stdout.trim().split(/\s+/);
|
|
25058
25266
|
if (parts.length === 2) {
|
|
@@ -25062,6 +25270,16 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
25062
25270
|
}
|
|
25063
25271
|
} catch {}
|
|
25064
25272
|
})(),
|
|
25273
|
+
(async () => {
|
|
25274
|
+
if (!stackEntry?.explicit || opts.dryRun)
|
|
25275
|
+
return;
|
|
25276
|
+
try {
|
|
25277
|
+
const currentBaseSha = await resolveCommitSha(repo.mainPath, stackEntry.baseRef, opts);
|
|
25278
|
+
row.baseChanged = currentBaseSha !== stackEntry.baseSha;
|
|
25279
|
+
} catch {
|
|
25280
|
+
row.baseChanged = true;
|
|
25281
|
+
}
|
|
25282
|
+
})(),
|
|
25065
25283
|
(async () => {
|
|
25066
25284
|
row.rebaseStatus = detectInProgressRebase(wt.path);
|
|
25067
25285
|
})(),
|
|
@@ -25073,7 +25291,6 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
25073
25291
|
})(),
|
|
25074
25292
|
(async () => {
|
|
25075
25293
|
if (branch && config2.user) {
|
|
25076
|
-
const pr = prMap.get(branch);
|
|
25077
25294
|
try {
|
|
25078
25295
|
const owner = await resolveOwnership({
|
|
25079
25296
|
configUser: config2.user,
|
|
@@ -25090,7 +25307,6 @@ async function fetchWorktreeData(opts, scope) {
|
|
|
25090
25307
|
})()
|
|
25091
25308
|
]);
|
|
25092
25309
|
if (branch) {
|
|
25093
|
-
const pr = prMap.get(branch);
|
|
25094
25310
|
if (pr) {
|
|
25095
25311
|
row.prNumber = pr.number;
|
|
25096
25312
|
row.prUrl = pr.url;
|
|
@@ -25127,6 +25343,7 @@ var init_data = __esm(() => {
|
|
|
25127
25343
|
init_forge();
|
|
25128
25344
|
init_owner();
|
|
25129
25345
|
init_types2();
|
|
25346
|
+
init_stack();
|
|
25130
25347
|
prCache = new Map;
|
|
25131
25348
|
});
|
|
25132
25349
|
|
|
@@ -25147,14 +25364,16 @@ function matchesFilter(entry, term) {
|
|
|
25147
25364
|
return true;
|
|
25148
25365
|
if (entry.prUrl?.toLowerCase().includes(lower))
|
|
25149
25366
|
return true;
|
|
25367
|
+
if (entry.base?.toLowerCase().includes(lower))
|
|
25368
|
+
return true;
|
|
25150
25369
|
return false;
|
|
25151
25370
|
}
|
|
25152
|
-
function toggleSelection(current,
|
|
25371
|
+
function toggleSelection(current, path36) {
|
|
25153
25372
|
const next = new Set(current);
|
|
25154
|
-
if (next.has(
|
|
25155
|
-
next.delete(
|
|
25373
|
+
if (next.has(path36)) {
|
|
25374
|
+
next.delete(path36);
|
|
25156
25375
|
} else {
|
|
25157
|
-
next.add(
|
|
25376
|
+
next.add(path36);
|
|
25158
25377
|
}
|
|
25159
25378
|
return next;
|
|
25160
25379
|
}
|
|
@@ -25181,6 +25400,13 @@ function rowSort(a2, b) {
|
|
|
25181
25400
|
return 1;
|
|
25182
25401
|
return a2.branch.localeCompare(b.branch);
|
|
25183
25402
|
}
|
|
25403
|
+
function sortRowsHierarchically(rows) {
|
|
25404
|
+
return buildStackHierarchy(rows, (row) => row.branch, (row) => row.base, rowSort).map(({ item, depth, prefix }) => ({
|
|
25405
|
+
...item,
|
|
25406
|
+
hierarchyDepth: depth,
|
|
25407
|
+
hierarchyPrefix: prefix
|
|
25408
|
+
}));
|
|
25409
|
+
}
|
|
25184
25410
|
function sortBlocks(blocks) {
|
|
25185
25411
|
return [...blocks].sort((a2, b) => a2.repoName.localeCompare(b.repoName));
|
|
25186
25412
|
}
|
|
@@ -25236,9 +25462,12 @@ function withCreatePlaceholders(blocks, creating) {
|
|
|
25236
25462
|
if (!branches)
|
|
25237
25463
|
return block;
|
|
25238
25464
|
const placeholders = branches.map((br) => makePlaceholderRow(block.repoName, br));
|
|
25239
|
-
return { ...block, rows: [...block.rows, ...placeholders]
|
|
25465
|
+
return { ...block, rows: sortRowsHierarchically([...block.rows, ...placeholders]) };
|
|
25240
25466
|
});
|
|
25241
25467
|
}
|
|
25468
|
+
var init_utils = __esm(() => {
|
|
25469
|
+
init_stack();
|
|
25470
|
+
});
|
|
25242
25471
|
|
|
25243
25472
|
// src/tui/hooks/useWorktrees.ts
|
|
25244
25473
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
@@ -25278,8 +25507,7 @@ function useWorktrees(opts) {
|
|
|
25278
25507
|
}
|
|
25279
25508
|
const newBlocks = [];
|
|
25280
25509
|
for (const [repoName, rows] of byRepo.entries()) {
|
|
25281
|
-
|
|
25282
|
-
newBlocks.push({ repoName, rows });
|
|
25510
|
+
newBlocks.push({ repoName, rows: sortRowsHierarchically(rows) });
|
|
25283
25511
|
}
|
|
25284
25512
|
newBlocks.sort((a2, b) => a2.repoName.localeCompare(b.repoName));
|
|
25285
25513
|
const scopeSet = scope ? new Set(scope) : undefined;
|
|
@@ -25306,6 +25534,7 @@ function useWorktrees(opts) {
|
|
|
25306
25534
|
var init_useWorktrees = __esm(() => {
|
|
25307
25535
|
init_data();
|
|
25308
25536
|
init_config();
|
|
25537
|
+
init_utils();
|
|
25309
25538
|
});
|
|
25310
25539
|
|
|
25311
25540
|
// src/tui/theme.ts
|
|
@@ -25366,12 +25595,17 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
|
|
|
25366
25595
|
row.prChecks ? `(${row.prChecks})` : ""
|
|
25367
25596
|
].filter(Boolean).join(" ") : "";
|
|
25368
25597
|
const ownerSegment = row.owner ? ` · by ${row.owner}` : "";
|
|
25598
|
+
const baseSegment = row.base ? ` · base ${row.base}` : "";
|
|
25369
25599
|
const rebaseSegment = !row.isMainCheckout && row.rebaseStatus && !row.isPrunable ? ` · ${row.rebaseStatus}` : "";
|
|
25600
|
+
const baseChangedSegment = row.baseChanged ? " · base moved" : "";
|
|
25601
|
+
const hierarchyPrefix = row.hierarchyPrefix ?? "";
|
|
25602
|
+
const secondaryIndent = " ".repeat(SECONDARY_INDENT.length + hierarchyPrefix.length);
|
|
25370
25603
|
const secondary = [
|
|
25371
|
-
`${
|
|
25604
|
+
`${secondaryIndent}${row.commitShort}`,
|
|
25372
25605
|
divergence,
|
|
25373
25606
|
prSegment,
|
|
25374
|
-
ownerSegment
|
|
25607
|
+
ownerSegment,
|
|
25608
|
+
baseSegment
|
|
25375
25609
|
].filter(Boolean).join(" ").trimEnd();
|
|
25376
25610
|
return /* @__PURE__ */ jsxs("box", {
|
|
25377
25611
|
id,
|
|
@@ -25389,6 +25623,10 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
|
|
|
25389
25623
|
fg: tokens.accent,
|
|
25390
25624
|
children: isMultiSelected ? "✓ " : " "
|
|
25391
25625
|
}),
|
|
25626
|
+
/* @__PURE__ */ jsx("span", {
|
|
25627
|
+
fg: tokens.dim,
|
|
25628
|
+
children: hierarchyPrefix
|
|
25629
|
+
}),
|
|
25392
25630
|
/* @__PURE__ */ jsx("span", {
|
|
25393
25631
|
fg: primary,
|
|
25394
25632
|
children: truncateBranch(row.branch)
|
|
@@ -25405,6 +25643,10 @@ function WorktreeItem({ row, isSelected, isMultiSelected, indicator, frame, id }
|
|
|
25405
25643
|
fg: tokens.dim,
|
|
25406
25644
|
children: secondary
|
|
25407
25645
|
}),
|
|
25646
|
+
baseChangedSegment && /* @__PURE__ */ jsx("span", {
|
|
25647
|
+
fg: tokens.warning,
|
|
25648
|
+
children: baseChangedSegment
|
|
25649
|
+
}),
|
|
25408
25650
|
rebaseSegment && /* @__PURE__ */ jsx("span", {
|
|
25409
25651
|
fg: tokens.error,
|
|
25410
25652
|
children: rebaseSegment
|
|
@@ -25510,7 +25752,7 @@ function DetailPane({ selectedRow }) {
|
|
|
25510
25752
|
const {
|
|
25511
25753
|
repoName,
|
|
25512
25754
|
branch,
|
|
25513
|
-
path:
|
|
25755
|
+
path: path36,
|
|
25514
25756
|
commitShort,
|
|
25515
25757
|
isMainCheckout,
|
|
25516
25758
|
isLocked,
|
|
@@ -25524,7 +25766,9 @@ function DetailPane({ selectedRow }) {
|
|
|
25524
25766
|
prUrl,
|
|
25525
25767
|
owner,
|
|
25526
25768
|
rebaseStatus,
|
|
25527
|
-
depsStrategy
|
|
25769
|
+
depsStrategy,
|
|
25770
|
+
base: base2,
|
|
25771
|
+
baseChanged
|
|
25528
25772
|
} = selectedRow;
|
|
25529
25773
|
const aheadBehindStr = ahead !== null && behind !== null ? `${ahead} ahead, ${behind} behind` : "unknown";
|
|
25530
25774
|
return /* @__PURE__ */ jsxs2("scrollbox", {
|
|
@@ -25579,7 +25823,7 @@ function DetailPane({ selectedRow }) {
|
|
|
25579
25823
|
fg: tokens.fg,
|
|
25580
25824
|
children: [
|
|
25581
25825
|
" ",
|
|
25582
|
-
|
|
25826
|
+
path36
|
|
25583
25827
|
]
|
|
25584
25828
|
})
|
|
25585
25829
|
]
|
|
@@ -25629,12 +25873,39 @@ function DetailPane({ selectedRow }) {
|
|
|
25629
25873
|
})
|
|
25630
25874
|
]
|
|
25631
25875
|
}),
|
|
25876
|
+
base2 && /* @__PURE__ */ jsxs2("text", {
|
|
25877
|
+
children: [
|
|
25878
|
+
/* @__PURE__ */ jsx2("span", {
|
|
25879
|
+
fg: tokens.dim,
|
|
25880
|
+
children: "Base:"
|
|
25881
|
+
}),
|
|
25882
|
+
/* @__PURE__ */ jsxs2("span", {
|
|
25883
|
+
fg: tokens.accent,
|
|
25884
|
+
children: [
|
|
25885
|
+
" ",
|
|
25886
|
+
base2
|
|
25887
|
+
]
|
|
25888
|
+
})
|
|
25889
|
+
]
|
|
25890
|
+
}),
|
|
25891
|
+
baseChanged && /* @__PURE__ */ jsxs2("text", {
|
|
25892
|
+
children: [
|
|
25893
|
+
/* @__PURE__ */ jsx2("span", {
|
|
25894
|
+
fg: tokens.dim,
|
|
25895
|
+
children: "Base state:"
|
|
25896
|
+
}),
|
|
25897
|
+
/* @__PURE__ */ jsx2("span", {
|
|
25898
|
+
fg: tokens.warning,
|
|
25899
|
+
children: " moved since recorded"
|
|
25900
|
+
})
|
|
25901
|
+
]
|
|
25902
|
+
}),
|
|
25632
25903
|
/* @__PURE__ */ jsxs2("text", {
|
|
25633
25904
|
style: { marginTop: 1 },
|
|
25634
25905
|
children: [
|
|
25635
25906
|
/* @__PURE__ */ jsx2("span", {
|
|
25636
25907
|
fg: tokens.dim,
|
|
25637
|
-
children: "vs main:"
|
|
25908
|
+
children: base2 ? "vs base:" : "vs main:"
|
|
25638
25909
|
}),
|
|
25639
25910
|
/* @__PURE__ */ jsxs2("span", {
|
|
25640
25911
|
fg: tokens.fg,
|
|
@@ -25960,9 +26231,9 @@ var init_HelpOverlay = __esm(() => {
|
|
|
25960
26231
|
["n", "Create new worktree (pick dependency strategy)"],
|
|
25961
26232
|
["f", "Fetch main for selected repo(s)"],
|
|
25962
26233
|
["p", "Pull latest changes for selected branch(es)"],
|
|
25963
|
-
["b", "Rebase selected onto main"],
|
|
26234
|
+
["b", "Rebase selected onto base (or main)"],
|
|
25964
26235
|
["s", "Sync selected (env files + hooks)"],
|
|
25965
|
-
["i", "Install dependencies in
|
|
26236
|
+
["i", "Install dependencies in selection (worktrees and main)"],
|
|
25966
26237
|
["m", "Rename selected worktree (branch + directory)"],
|
|
25967
26238
|
["o", "Open selected in IDE"],
|
|
25968
26239
|
["a", "Spawn agent in selected"],
|
|
@@ -26136,38 +26407,38 @@ var init_ActionLogModal = __esm(() => {
|
|
|
26136
26407
|
});
|
|
26137
26408
|
|
|
26138
26409
|
// src/lib/history.ts
|
|
26139
|
-
import
|
|
26140
|
-
import
|
|
26410
|
+
import fs31 from "fs";
|
|
26411
|
+
import path36 from "path";
|
|
26141
26412
|
import os4 from "os";
|
|
26142
26413
|
function getHistoryDir() {
|
|
26143
|
-
const stateHome = process.env.XDG_STATE_HOME ??
|
|
26144
|
-
return
|
|
26414
|
+
const stateHome = process.env.XDG_STATE_HOME ?? path36.join(os4.homedir(), ".local", "state");
|
|
26415
|
+
return path36.join(stateHome, "wtx");
|
|
26145
26416
|
}
|
|
26146
26417
|
function getHistoryPath() {
|
|
26147
|
-
return
|
|
26418
|
+
return path36.join(getHistoryDir(), "history.jsonl");
|
|
26148
26419
|
}
|
|
26149
26420
|
function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_KEEP_LINES) {
|
|
26150
26421
|
const historyPath = getHistoryPath();
|
|
26151
26422
|
let size = 0;
|
|
26152
26423
|
try {
|
|
26153
|
-
size =
|
|
26424
|
+
size = fs31.statSync(historyPath).size;
|
|
26154
26425
|
} catch {
|
|
26155
26426
|
return;
|
|
26156
26427
|
}
|
|
26157
26428
|
if (size <= maxBytes)
|
|
26158
26429
|
return;
|
|
26159
|
-
const lines =
|
|
26430
|
+
const lines = fs31.readFileSync(historyPath, "utf-8").split(`
|
|
26160
26431
|
`).filter(Boolean);
|
|
26161
26432
|
const kept = lines.slice(-keepLines);
|
|
26162
26433
|
const tmpPath = `${historyPath}.tmp.${process.pid}`;
|
|
26163
26434
|
try {
|
|
26164
|
-
|
|
26435
|
+
fs31.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
|
|
26165
26436
|
`)}
|
|
26166
26437
|
` : "", "utf-8");
|
|
26167
|
-
|
|
26438
|
+
fs31.renameSync(tmpPath, historyPath);
|
|
26168
26439
|
} catch (err) {
|
|
26169
|
-
if (
|
|
26170
|
-
|
|
26440
|
+
if (fs31.existsSync(tmpPath)) {
|
|
26441
|
+
fs31.unlinkSync(tmpPath);
|
|
26171
26442
|
}
|
|
26172
26443
|
throw err;
|
|
26173
26444
|
}
|
|
@@ -26175,18 +26446,18 @@ function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_
|
|
|
26175
26446
|
function appendHistory(entry) {
|
|
26176
26447
|
try {
|
|
26177
26448
|
const dir = getHistoryDir();
|
|
26178
|
-
if (!
|
|
26179
|
-
|
|
26449
|
+
if (!fs31.existsSync(dir)) {
|
|
26450
|
+
fs31.mkdirSync(dir, { recursive: true });
|
|
26180
26451
|
}
|
|
26181
26452
|
rotateHistory();
|
|
26182
|
-
|
|
26453
|
+
fs31.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
|
|
26183
26454
|
`, "utf-8");
|
|
26184
26455
|
} catch {}
|
|
26185
26456
|
}
|
|
26186
26457
|
function readRecentHistory(limit = 50) {
|
|
26187
26458
|
let content;
|
|
26188
26459
|
try {
|
|
26189
|
-
content =
|
|
26460
|
+
content = fs31.readFileSync(getHistoryPath(), "utf-8");
|
|
26190
26461
|
} catch {
|
|
26191
26462
|
return [];
|
|
26192
26463
|
}
|
|
@@ -26787,6 +27058,7 @@ function ConfigOverlay({ onClose, onSaved, onError }) {
|
|
|
26787
27058
|
}
|
|
26788
27059
|
var init_ConfigOverlay = __esm(() => {
|
|
26789
27060
|
init_Overlay();
|
|
27061
|
+
init_utils();
|
|
26790
27062
|
init_theme();
|
|
26791
27063
|
init_config();
|
|
26792
27064
|
init_InputModal();
|
|
@@ -26835,6 +27107,8 @@ function App({ opts }) {
|
|
|
26835
27107
|
const nextOpId = useRef3(1);
|
|
26836
27108
|
const [createModal, setCreateModal] = useState7(false);
|
|
26837
27109
|
const [createError, setCreateError] = useState7();
|
|
27110
|
+
const [createBaseModal, setCreateBaseModal] = useState7(null);
|
|
27111
|
+
const [createBaseError, setCreateBaseError] = useState7();
|
|
26838
27112
|
const [createDepsChoice, setCreateDepsChoice] = useState7(null);
|
|
26839
27113
|
const [renameModal, setRenameModal] = useState7(false);
|
|
26840
27114
|
const [renameError, setRenameError] = useState7();
|
|
@@ -26990,7 +27264,7 @@ function App({ opts }) {
|
|
|
26990
27264
|
setOps((prev) => [...prev, op]);
|
|
26991
27265
|
executeOp(op, ["fetch", "--repo", repoNames.join(",")], repoNames).then(() => setSelection(new Set));
|
|
26992
27266
|
};
|
|
26993
|
-
const startCreate = (branch, repoName, deps) => {
|
|
27267
|
+
const startCreate = (branch, repoName, deps, base2) => {
|
|
26994
27268
|
const op = {
|
|
26995
27269
|
id: nextOpId.current++,
|
|
26996
27270
|
kind: "create",
|
|
@@ -27003,6 +27277,8 @@ function App({ opts }) {
|
|
|
27003
27277
|
};
|
|
27004
27278
|
setOps((prev) => [...prev, op]);
|
|
27005
27279
|
const args = ["create", branch, "--repo", repoName];
|
|
27280
|
+
if (base2)
|
|
27281
|
+
args.push("--base", base2);
|
|
27006
27282
|
if (deps && deps !== "auto")
|
|
27007
27283
|
args.push("--deps", deps);
|
|
27008
27284
|
executeOp(op, args, [repoName]);
|
|
@@ -27031,6 +27307,13 @@ function App({ opts }) {
|
|
|
27031
27307
|
}
|
|
27032
27308
|
return;
|
|
27033
27309
|
}
|
|
27310
|
+
if (createBaseModal) {
|
|
27311
|
+
if (key.name === "escape") {
|
|
27312
|
+
setCreateBaseModal(null);
|
|
27313
|
+
setCreateBaseError(undefined);
|
|
27314
|
+
}
|
|
27315
|
+
return;
|
|
27316
|
+
}
|
|
27034
27317
|
if (createDepsChoice)
|
|
27035
27318
|
return;
|
|
27036
27319
|
if (renameModal) {
|
|
@@ -27154,16 +27437,12 @@ function App({ opts }) {
|
|
|
27154
27437
|
const targets = getSelectedRows();
|
|
27155
27438
|
if (targets.length === 0)
|
|
27156
27439
|
return;
|
|
27157
|
-
if (targets.some((r) => r.isMainCheckout)) {
|
|
27158
|
-
flash("Cannot install deps on main checkout");
|
|
27159
|
-
return;
|
|
27160
|
-
}
|
|
27161
27440
|
const conflict = findConflict(targets);
|
|
27162
27441
|
if (conflict) {
|
|
27163
27442
|
flash(conflict);
|
|
27164
27443
|
return;
|
|
27165
27444
|
}
|
|
27166
|
-
startBatchActions("install", targets, (r) => ["deps", r.branch, "--repo", r.repoName, "--install"]);
|
|
27445
|
+
startBatchActions("install", targets, (r) => r.isMainCheckout ? ["deps", "--repo", r.repoName, "--install"] : ["deps", r.branch, "--repo", r.repoName, "--install"]);
|
|
27167
27446
|
return;
|
|
27168
27447
|
}
|
|
27169
27448
|
if (key.name === "m") {
|
|
@@ -27414,16 +27693,33 @@ function App({ opts }) {
|
|
|
27414
27693
|
}
|
|
27415
27694
|
setCreateModal(false);
|
|
27416
27695
|
setCreateError(undefined);
|
|
27417
|
-
|
|
27696
|
+
setCreateBaseError(undefined);
|
|
27697
|
+
setCreateBaseModal({ branch, repoName });
|
|
27698
|
+
}
|
|
27699
|
+
}),
|
|
27700
|
+
createBaseModal && /* @__PURE__ */ jsx12(InputModal, {
|
|
27701
|
+
title: `Base ref for ${createBaseModal.branch}`,
|
|
27702
|
+
placeholder: "origin/main (empty for default main)",
|
|
27703
|
+
errorMessage: createBaseError,
|
|
27704
|
+
onSubmit: (value) => {
|
|
27705
|
+
const base2 = value.trim();
|
|
27706
|
+
if (base2 && !validateSafeBranchName(base2)) {
|
|
27707
|
+
setCreateBaseError("Invalid base ref");
|
|
27708
|
+
return;
|
|
27709
|
+
}
|
|
27710
|
+
const { branch, repoName } = createBaseModal;
|
|
27711
|
+
setCreateBaseModal(null);
|
|
27712
|
+
setCreateBaseError(undefined);
|
|
27713
|
+
setCreateDepsChoice({ branch, repoName, base: base2 || undefined });
|
|
27418
27714
|
}
|
|
27419
27715
|
}),
|
|
27420
27716
|
createDepsChoice && /* @__PURE__ */ jsx12(ChoiceModal, {
|
|
27421
27717
|
title: `Dependencies for ${createDepsChoice.branch}`,
|
|
27422
27718
|
options: DEPS_CHOICES,
|
|
27423
27719
|
onSubmit: (choice) => {
|
|
27424
|
-
const { branch, repoName } = createDepsChoice;
|
|
27720
|
+
const { branch, repoName, base: base2 } = createDepsChoice;
|
|
27425
27721
|
setCreateDepsChoice(null);
|
|
27426
|
-
startCreate(branch, repoName, choice);
|
|
27722
|
+
startCreate(branch, repoName, choice, base2);
|
|
27427
27723
|
},
|
|
27428
27724
|
onCancel: () => setCreateDepsChoice(null)
|
|
27429
27725
|
}),
|
|
@@ -27493,6 +27789,7 @@ var init_App = __esm(() => {
|
|
|
27493
27789
|
init_InputModal();
|
|
27494
27790
|
init_ChoiceModal();
|
|
27495
27791
|
init_ConfigOverlay();
|
|
27792
|
+
init_utils();
|
|
27496
27793
|
init_agents();
|
|
27497
27794
|
init_config();
|
|
27498
27795
|
init_history();
|
|
@@ -29661,8 +29958,8 @@ Run 'wtx config show' to see available repos.`);
|
|
|
29661
29958
|
init_config();
|
|
29662
29959
|
init_log();
|
|
29663
29960
|
init_git();
|
|
29664
|
-
import
|
|
29665
|
-
import
|
|
29961
|
+
import fs21 from "fs";
|
|
29962
|
+
import path27 from "path";
|
|
29666
29963
|
|
|
29667
29964
|
// src/lib/remotes.ts
|
|
29668
29965
|
init_execa();
|
|
@@ -29727,6 +30024,7 @@ function resolveBranchTarget(input) {
|
|
|
29727
30024
|
}
|
|
29728
30025
|
|
|
29729
30026
|
// src/commands/create.ts
|
|
30027
|
+
init_stack();
|
|
29730
30028
|
init_resolver();
|
|
29731
30029
|
|
|
29732
30030
|
// src/lib/ide.ts
|
|
@@ -29742,14 +30040,14 @@ function spawnIde(ide, wtPath) {
|
|
|
29742
30040
|
// src/lib/worktree-setup.ts
|
|
29743
30041
|
init_execa();
|
|
29744
30042
|
init_log();
|
|
29745
|
-
import
|
|
29746
|
-
import
|
|
30043
|
+
import fs10 from "fs";
|
|
30044
|
+
import path17 from "path";
|
|
29747
30045
|
|
|
29748
30046
|
// src/lib/ports.ts
|
|
29749
30047
|
init_git();
|
|
29750
30048
|
init_path_safety();
|
|
29751
30049
|
init_config();
|
|
29752
|
-
import
|
|
30050
|
+
import fs9 from "fs";
|
|
29753
30051
|
function hashPort(key, min, max) {
|
|
29754
30052
|
let h2 = 2166136261;
|
|
29755
30053
|
for (let i2 = 0;i2 < key.length; i2++) {
|
|
@@ -29784,7 +30082,7 @@ async function getWorktreePort(repoName, branch, config2, currentWtPath) {
|
|
|
29784
30082
|
const taken = new Set;
|
|
29785
30083
|
for (const name of allRepos) {
|
|
29786
30084
|
const mainPath = `${root}/${name}`;
|
|
29787
|
-
if (!
|
|
30085
|
+
if (!fs9.existsSync(mainPath)) {
|
|
29788
30086
|
continue;
|
|
29789
30087
|
}
|
|
29790
30088
|
let wts = [];
|
|
@@ -29815,12 +30113,12 @@ async function runPostCreateSetup(params) {
|
|
|
29815
30113
|
let hooks = [];
|
|
29816
30114
|
if (repo.config.sync_files && repo.config.sync_files.length > 0) {
|
|
29817
30115
|
for (const file2 of repo.config.sync_files) {
|
|
29818
|
-
const src =
|
|
29819
|
-
const dest =
|
|
29820
|
-
if (
|
|
30116
|
+
const src = path17.join(repo.mainPath, file2);
|
|
30117
|
+
const dest = path17.join(wtPath, file2);
|
|
30118
|
+
if (fs10.existsSync(src)) {
|
|
29821
30119
|
if (!globalOpts.dryRun) {
|
|
29822
|
-
|
|
29823
|
-
|
|
30120
|
+
fs10.mkdirSync(path17.dirname(dest), { recursive: true });
|
|
30121
|
+
fs10.copyFileSync(src, dest);
|
|
29824
30122
|
copiedFiles.push(file2);
|
|
29825
30123
|
}
|
|
29826
30124
|
stepSuccess(`Synced ${file2}`);
|
|
@@ -29948,9 +30246,6 @@ function registerCreateCommand(program2) {
|
|
|
29948
30246
|
openWorktree(wtPath);
|
|
29949
30247
|
continue;
|
|
29950
30248
|
}
|
|
29951
|
-
if (!globalOpts.dryRun) {
|
|
29952
|
-
fs20.mkdirSync(path26.dirname(wtPath), { recursive: true });
|
|
29953
|
-
}
|
|
29954
30249
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
29955
30250
|
const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
|
|
29956
30251
|
if (repo.config.fetch_main_on_create) {
|
|
@@ -29961,6 +30256,19 @@ function registerCreateCommand(program2) {
|
|
|
29961
30256
|
}
|
|
29962
30257
|
stepProgress("Checking branch status...");
|
|
29963
30258
|
const baseRef = options.base || `${resolvedRemote}/${mainBranch}`;
|
|
30259
|
+
if (baseRef === branch || baseRef === `refs/heads/${branch}`) {
|
|
30260
|
+
throw new Error(`Base ref '${baseRef}' cannot be the new branch '${branch}'`);
|
|
30261
|
+
}
|
|
30262
|
+
let baseSha = null;
|
|
30263
|
+
if (!globalOpts.dryRun) {
|
|
30264
|
+
baseSha = await resolveCommitSha(repo.mainPath, baseRef, globalOpts);
|
|
30265
|
+
stepSuccess("Base resolved", `${baseRef} at ${baseSha.substring(0, 7)}`);
|
|
30266
|
+
} else {
|
|
30267
|
+
stepProgress("Using base", baseRef);
|
|
30268
|
+
}
|
|
30269
|
+
if (!globalOpts.dryRun) {
|
|
30270
|
+
fs21.mkdirSync(path27.dirname(wtPath), { recursive: true });
|
|
30271
|
+
}
|
|
29964
30272
|
const localSha = await getLocalBranchSha(repo.mainPath, branch, globalOpts);
|
|
29965
30273
|
const remoteSha = await getRemoteBranchSha(repo.mainPath, resolvedRemote, branch, globalOpts);
|
|
29966
30274
|
const localExists = localSha !== null;
|
|
@@ -30021,6 +30329,21 @@ function registerCreateCommand(program2) {
|
|
|
30021
30329
|
throw err;
|
|
30022
30330
|
}
|
|
30023
30331
|
stepSuccess("Worktree created", wtPath);
|
|
30332
|
+
if (!globalOpts.dryRun && baseSha && resolvedAction.kind === "create-new") {
|
|
30333
|
+
try {
|
|
30334
|
+
const metadataBaseRef = options.base ? baseRef : mainBranch;
|
|
30335
|
+
await recordStackEntry(repo.mainPath, branch, {
|
|
30336
|
+
baseRef: metadataBaseRef,
|
|
30337
|
+
baseSha,
|
|
30338
|
+
explicit: options.base !== undefined,
|
|
30339
|
+
createdAt: new Date().toISOString()
|
|
30340
|
+
}, globalOpts);
|
|
30341
|
+
stepSuccess("Base recorded", metadataBaseRef);
|
|
30342
|
+
} catch (err) {
|
|
30343
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30344
|
+
stepWarning("Base metadata not recorded", message);
|
|
30345
|
+
}
|
|
30346
|
+
}
|
|
30024
30347
|
const setupResult = await runPostCreateSetup({ config: config2, repo, wtPath, branch, globalOpts });
|
|
30025
30348
|
if (options.deps && options.deps !== "auto" && options.deps !== "link" && options.deps !== "off") {
|
|
30026
30349
|
const ok = await applyDepsStrategy(options.deps, wtPath, repo.mainPath, globalOpts);
|
|
@@ -30088,9 +30411,10 @@ init_config();
|
|
|
30088
30411
|
init_log();
|
|
30089
30412
|
init_git();
|
|
30090
30413
|
init_resolver();
|
|
30091
|
-
import
|
|
30092
|
-
import
|
|
30414
|
+
import fs22 from "fs";
|
|
30415
|
+
import path28 from "path";
|
|
30093
30416
|
init_forge();
|
|
30417
|
+
init_stack();
|
|
30094
30418
|
function registerPullCommand(program2) {
|
|
30095
30419
|
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
30420
|
const globalOpts = program2.opts();
|
|
@@ -30112,7 +30436,7 @@ function registerPullCommand(program2) {
|
|
|
30112
30436
|
for (const name of Object.keys(config2.repos).sort()) {
|
|
30113
30437
|
if (config2.repos[name].check_prs === false)
|
|
30114
30438
|
continue;
|
|
30115
|
-
const mainPath =
|
|
30439
|
+
const mainPath = path28.join(expandTilde(config2.root), name);
|
|
30116
30440
|
const detected = detectRepoForge(mainPath);
|
|
30117
30441
|
if (!detected)
|
|
30118
30442
|
continue;
|
|
@@ -30210,10 +30534,22 @@ function registerPullCommand(program2) {
|
|
|
30210
30534
|
summaryWarning(`Nothing pulled — branch '${branch}' already exists`);
|
|
30211
30535
|
return;
|
|
30212
30536
|
}
|
|
30213
|
-
const
|
|
30537
|
+
const mainBranch = await resolveMainBranch(target, config2);
|
|
30538
|
+
const baseRemote = await resolveBaseRemote(target.mainPath, mainBranch);
|
|
30539
|
+
const baseBranch = head.baseRefName || mainBranch;
|
|
30540
|
+
const baseRef = `${baseRemote}/${baseBranch}`;
|
|
30214
30541
|
const fetch = adapter.buildHeadFetch(head);
|
|
30215
30542
|
stepProgress(fetch.url ? `Fetching ${fetch.refspec} from fork...` : `Fetching pull/${head.number}/head from ${baseRemote}...`);
|
|
30216
30543
|
try {
|
|
30544
|
+
if (baseBranch !== mainBranch) {
|
|
30545
|
+
try {
|
|
30546
|
+
await gitExec(["-C", target.mainPath, "fetch", baseRemote, "--", baseBranch], { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun });
|
|
30547
|
+
} catch (err) {
|
|
30548
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30549
|
+
stepWarning("PR base was not fetched", message.split(`
|
|
30550
|
+
`)[0] ?? message);
|
|
30551
|
+
}
|
|
30552
|
+
}
|
|
30217
30553
|
await gitExec([
|
|
30218
30554
|
"-C",
|
|
30219
30555
|
target.mainPath,
|
|
@@ -30227,7 +30563,7 @@ function registerPullCommand(program2) {
|
|
|
30227
30563
|
}
|
|
30228
30564
|
stepSuccess("Fetched");
|
|
30229
30565
|
if (!globalOpts.dryRun) {
|
|
30230
|
-
|
|
30566
|
+
fs22.mkdirSync(path28.dirname(wtPath), { recursive: true });
|
|
30231
30567
|
}
|
|
30232
30568
|
try {
|
|
30233
30569
|
await gitExec(["-C", target.mainPath, "worktree", "add", "-b", branch, wtPath, "FETCH_HEAD"], { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun });
|
|
@@ -30241,6 +30577,22 @@ function registerPullCommand(program2) {
|
|
|
30241
30577
|
throw err;
|
|
30242
30578
|
}
|
|
30243
30579
|
stepSuccess("Worktree created", wtPath);
|
|
30580
|
+
if (!globalOpts.dryRun) {
|
|
30581
|
+
try {
|
|
30582
|
+
const baseSha = await resolveCommitSha(target.mainPath, baseRef, globalOpts);
|
|
30583
|
+
const metadataBaseRef = baseBranch === mainBranch ? mainBranch : baseRef;
|
|
30584
|
+
await recordStackEntry(target.mainPath, branch, {
|
|
30585
|
+
baseRef: metadataBaseRef,
|
|
30586
|
+
baseSha,
|
|
30587
|
+
explicit: baseBranch !== mainBranch,
|
|
30588
|
+
createdAt: new Date().toISOString()
|
|
30589
|
+
}, globalOpts);
|
|
30590
|
+
stepSuccess("Base recorded", metadataBaseRef);
|
|
30591
|
+
} catch (err) {
|
|
30592
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30593
|
+
stepWarning("Base metadata not recorded", message);
|
|
30594
|
+
}
|
|
30595
|
+
}
|
|
30244
30596
|
const setupResult = await runPostCreateSetup({ config: config2, repo: target, wtPath, branch, globalOpts });
|
|
30245
30597
|
const failedHooks = setupResult.hooks.filter((h2) => !h2.ok);
|
|
30246
30598
|
if (failedHooks.length > 0) {
|
|
@@ -30346,7 +30698,7 @@ init_log();
|
|
|
30346
30698
|
init_git();
|
|
30347
30699
|
init_resolver();
|
|
30348
30700
|
init_path_safety();
|
|
30349
|
-
import
|
|
30701
|
+
import path29 from "path";
|
|
30350
30702
|
|
|
30351
30703
|
// src/lib/prompts.ts
|
|
30352
30704
|
import * as readline2 from "readline";
|
|
@@ -30389,6 +30741,7 @@ async function confirm(message, io) {
|
|
|
30389
30741
|
}
|
|
30390
30742
|
|
|
30391
30743
|
// src/commands/remove.ts
|
|
30744
|
+
init_stack();
|
|
30392
30745
|
function registerRemoveCommand(program2) {
|
|
30393
30746
|
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
30747
|
const globalOpts = program2.opts();
|
|
@@ -30413,6 +30766,16 @@ function registerRemoveCommand(program2) {
|
|
|
30413
30766
|
continue;
|
|
30414
30767
|
}
|
|
30415
30768
|
const wtPath = target.path;
|
|
30769
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, globalOpts);
|
|
30770
|
+
const children = getStackChildren(stackMetadata, branch);
|
|
30771
|
+
if (children.length > 0 && !options.force) {
|
|
30772
|
+
stepError("Worktree has dependent branches", `${children.join(", ")} — retarget or remove them first (use --force to override)`);
|
|
30773
|
+
skipCount++;
|
|
30774
|
+
continue;
|
|
30775
|
+
}
|
|
30776
|
+
if (children.length > 0) {
|
|
30777
|
+
stepWarning("Removing a parent with dependent branches", children.join(", "));
|
|
30778
|
+
}
|
|
30416
30779
|
if (!options.force && !globalOpts.dryRun) {
|
|
30417
30780
|
try {
|
|
30418
30781
|
const dirtyFiles = await getDirtyFiles(wtPath);
|
|
@@ -30443,7 +30806,7 @@ function registerRemoveCommand(program2) {
|
|
|
30443
30806
|
const toClean = planEmptyParentRemoval(repo.wtRoot, repo.mainPath, wtPath);
|
|
30444
30807
|
indented(`Will remove worktree: ${wtPath}`);
|
|
30445
30808
|
for (const dir of toClean) {
|
|
30446
|
-
indented(`Will clean up empty dir: ${
|
|
30809
|
+
indented(`Will clean up empty dir: ${path29.relative(repo.wtRoot, dir)}/`);
|
|
30447
30810
|
}
|
|
30448
30811
|
const proceed = await confirm("Are you sure you want to delete these?");
|
|
30449
30812
|
if (!proceed) {
|
|
@@ -30473,9 +30836,18 @@ function registerRemoveCommand(program2) {
|
|
|
30473
30836
|
if (!globalOpts.dryRun) {
|
|
30474
30837
|
const removedDirs = cleanupEmptyParents(repo.wtRoot, repo.mainPath, wtPath);
|
|
30475
30838
|
for (const dir of removedDirs) {
|
|
30476
|
-
stepSuccess("Cleaned up empty directory",
|
|
30839
|
+
stepSuccess("Cleaned up empty directory", path29.relative(repo.wtRoot, dir) + "/");
|
|
30477
30840
|
}
|
|
30478
30841
|
}
|
|
30842
|
+
try {
|
|
30843
|
+
await removeStackEntry(repo.mainPath, branch, globalOpts);
|
|
30844
|
+
} catch (err) {
|
|
30845
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30846
|
+
stepWarning("Stack metadata not removed", message);
|
|
30847
|
+
}
|
|
30848
|
+
if (children.length > 0) {
|
|
30849
|
+
stepWarning("Dependent base metadata retained", children.join(", "));
|
|
30850
|
+
}
|
|
30479
30851
|
successCount++;
|
|
30480
30852
|
} catch (err) {
|
|
30481
30853
|
stepError("Failed to remove worktree", err.message);
|
|
@@ -30499,7 +30871,7 @@ init_log();
|
|
|
30499
30871
|
init_git();
|
|
30500
30872
|
init_resolver();
|
|
30501
30873
|
init_forge();
|
|
30502
|
-
import
|
|
30874
|
+
import path30 from "path";
|
|
30503
30875
|
|
|
30504
30876
|
// src/lib/prune.ts
|
|
30505
30877
|
function selectMergedCandidates(worktrees, mainPath, prMap) {
|
|
@@ -30517,6 +30889,7 @@ function selectMergedCandidates(worktrees, mainPath, prMap) {
|
|
|
30517
30889
|
|
|
30518
30890
|
// src/commands/prune.ts
|
|
30519
30891
|
init_path_safety();
|
|
30892
|
+
init_stack();
|
|
30520
30893
|
function registerPruneCommand(program2) {
|
|
30521
30894
|
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
30895
|
const globalOpts = program2.opts();
|
|
@@ -30540,6 +30913,7 @@ function registerPruneCommand(program2) {
|
|
|
30540
30913
|
}
|
|
30541
30914
|
try {
|
|
30542
30915
|
const worktrees = await getWorktreeList(repo.mainPath);
|
|
30916
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, globalOpts);
|
|
30543
30917
|
const branches = worktrees.filter((wt) => wt.path !== repo.mainPath && wt.branch).map((wt) => wt.branch);
|
|
30544
30918
|
if (branches.length === 0) {
|
|
30545
30919
|
continue;
|
|
@@ -30564,6 +30938,15 @@ function registerPruneCommand(program2) {
|
|
|
30564
30938
|
continue;
|
|
30565
30939
|
}
|
|
30566
30940
|
const wtInfo = worktrees.find((wt) => wt.path === candidate.path);
|
|
30941
|
+
const children = getStackChildren(stackMetadata, candidate.branch);
|
|
30942
|
+
if (children.length > 0 && !options.force) {
|
|
30943
|
+
stepWarning("Skipped — branch has dependent worktrees", `${label}: ${children.join(", ")}`);
|
|
30944
|
+
skippedCount++;
|
|
30945
|
+
continue;
|
|
30946
|
+
}
|
|
30947
|
+
if (children.length > 0) {
|
|
30948
|
+
stepWarning("Pruning parent with dependent worktrees", `${label}: ${children.join(", ")}`);
|
|
30949
|
+
}
|
|
30567
30950
|
if (wtInfo?.isLocked && !options.force) {
|
|
30568
30951
|
stepWarning("Skipped — worktree is locked \uD83D\uDD12", label);
|
|
30569
30952
|
skippedCount++;
|
|
@@ -30644,9 +31027,15 @@ function registerPruneCommand(program2) {
|
|
|
30644
31027
|
if (!globalOpts.dryRun) {
|
|
30645
31028
|
const removedDirs = cleanupEmptyParents(repo.wtRoot, repo.mainPath, candidate.path);
|
|
30646
31029
|
for (const dir of removedDirs) {
|
|
30647
|
-
stepSuccess("Cleaned up empty directory",
|
|
31030
|
+
stepSuccess("Cleaned up empty directory", path30.relative(repo.wtRoot, dir) + "/");
|
|
30648
31031
|
}
|
|
30649
31032
|
}
|
|
31033
|
+
try {
|
|
31034
|
+
await removeStackEntry(repo.mainPath, candidate.branch, globalOpts);
|
|
31035
|
+
} catch (err) {
|
|
31036
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31037
|
+
stepWarning("Stack metadata not removed", message);
|
|
31038
|
+
}
|
|
30650
31039
|
removedCount++;
|
|
30651
31040
|
}
|
|
30652
31041
|
if (removedCount === 0 && skippedCount === 0) {
|
|
@@ -30713,13 +31102,15 @@ function formatRelativeTime(isoTimestamp) {
|
|
|
30713
31102
|
// src/commands/ls.ts
|
|
30714
31103
|
init_owner();
|
|
30715
31104
|
init_source();
|
|
30716
|
-
|
|
30717
|
-
|
|
31105
|
+
init_stack();
|
|
31106
|
+
init_stack();
|
|
31107
|
+
import path31 from "path";
|
|
31108
|
+
import fs23 from "fs";
|
|
30718
31109
|
function buildLsJson(reposData) {
|
|
30719
31110
|
const result = [];
|
|
30720
31111
|
for (const repo of reposData) {
|
|
30721
31112
|
for (const wt of repo.worktrees) {
|
|
30722
|
-
const branch = wt.branch ||
|
|
31113
|
+
const branch = wt.branch || path31.basename(wt.path);
|
|
30723
31114
|
const sha = (wt.commit || "0000000").substring(0, 7);
|
|
30724
31115
|
let status = "clean";
|
|
30725
31116
|
if (wt.path === repo.mainPath) {
|
|
@@ -30746,6 +31137,8 @@ function buildLsJson(reposData) {
|
|
|
30746
31137
|
}
|
|
30747
31138
|
}
|
|
30748
31139
|
const entry = { repo: repo.name, branch, sha, status };
|
|
31140
|
+
if (wt.base)
|
|
31141
|
+
entry.base = wt.base;
|
|
30749
31142
|
if (pr)
|
|
30750
31143
|
entry.pr = pr;
|
|
30751
31144
|
if (ownerStr)
|
|
@@ -30784,7 +31177,7 @@ function registerLsCommand(program2) {
|
|
|
30784
31177
|
};
|
|
30785
31178
|
try {
|
|
30786
31179
|
const worktrees = await getWorktreeList(repo.mainPath);
|
|
30787
|
-
const
|
|
31180
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, globalOpts);
|
|
30788
31181
|
let prMap = null;
|
|
30789
31182
|
if (options.pr) {
|
|
30790
31183
|
const branches = worktrees.filter((wt) => wt.path !== repo.mainPath && wt.branch).map((wt) => wt.branch);
|
|
@@ -30799,9 +31192,20 @@ function registerLsCommand(program2) {
|
|
|
30799
31192
|
}
|
|
30800
31193
|
}
|
|
30801
31194
|
}
|
|
30802
|
-
|
|
30803
|
-
const branch = wt.branch ||
|
|
30804
|
-
|
|
31195
|
+
const displayItems = buildStackHierarchy(worktrees, (wt) => wt.branch || path31.basename(wt.path), (wt) => {
|
|
31196
|
+
const branch = wt.branch || path31.basename(wt.path);
|
|
31197
|
+
return stackMetadata.branches[branch]?.baseRef ?? prMap?.get(branch)?.baseRefName;
|
|
31198
|
+
}, (a2, b) => {
|
|
31199
|
+
if (a2.path === repo.mainPath && b.path !== repo.mainPath)
|
|
31200
|
+
return -1;
|
|
31201
|
+
if (a2.path !== repo.mainPath && b.path === repo.mainPath)
|
|
31202
|
+
return 1;
|
|
31203
|
+
return (a2.branch || path31.basename(a2.path)).localeCompare(b.branch || path31.basename(b.path));
|
|
31204
|
+
});
|
|
31205
|
+
const maxBranchLen = Math.max(0, ...displayItems.map(({ item, prefix }) => `${prefix}${item.branch || path31.basename(item.path)}`.length));
|
|
31206
|
+
for (const { item: wt, prefix } of displayItems) {
|
|
31207
|
+
const branch = wt.branch || path31.basename(wt.path);
|
|
31208
|
+
const paddedBranch = `${prefix}${branch}`.padEnd(maxBranchLen + 2);
|
|
30805
31209
|
const hash2 = (wt.commit || "0000000").substring(0, 7);
|
|
30806
31210
|
let statusStr = source_default.dim("clean");
|
|
30807
31211
|
let isMissing = false;
|
|
@@ -30811,7 +31215,7 @@ function registerLsCommand(program2) {
|
|
|
30811
31215
|
statusStr = source_default.blue("[main checkout]");
|
|
30812
31216
|
} else if (wt.isLocked) {
|
|
30813
31217
|
statusStr = source_default.red("locked \uD83D\uDD12");
|
|
30814
|
-
} else if (
|
|
31218
|
+
} else if (fs23.existsSync(wt.path)) {
|
|
30815
31219
|
try {
|
|
30816
31220
|
dirtyFiles = await getDirtyFiles(wt.path);
|
|
30817
31221
|
if (dirtyFiles.length > 0) {
|
|
@@ -30832,6 +31236,8 @@ function registerLsCommand(program2) {
|
|
|
30832
31236
|
prSegment = ` #${prInfo.number} ${renderDisplayState(display)} ${source_default.dim(prInfo.url)}`;
|
|
30833
31237
|
}
|
|
30834
31238
|
let ownerSuffix = "";
|
|
31239
|
+
const baseRef = wt.branch ? stackMetadata.branches[wt.branch]?.baseRef ?? prInfo?.baseRefName : prInfo?.baseRefName;
|
|
31240
|
+
const baseSuffix = baseRef ? ` base ${baseRef}` : "";
|
|
30835
31241
|
let ownership = null;
|
|
30836
31242
|
if (wt.path !== repo.mainPath) {
|
|
30837
31243
|
ownership = await resolveOwnership({
|
|
@@ -30851,13 +31257,14 @@ function registerLsCommand(program2) {
|
|
|
30851
31257
|
path: wt.path,
|
|
30852
31258
|
branch: wt.branch,
|
|
30853
31259
|
commit: wt.commit,
|
|
31260
|
+
base: baseRef,
|
|
30854
31261
|
isLocked: wt.isLocked,
|
|
30855
31262
|
dirtyFiles,
|
|
30856
31263
|
isMissing,
|
|
30857
31264
|
isError
|
|
30858
31265
|
});
|
|
30859
31266
|
if (!options.json) {
|
|
30860
|
-
info(` ${paddedBranch} ${hash2} ${statusStr}${prSegment}${ownerSuffix}`);
|
|
31267
|
+
info(` ${paddedBranch} ${hash2} ${statusStr}${baseSuffix}${prSegment}${ownerSuffix}`);
|
|
30861
31268
|
}
|
|
30862
31269
|
}
|
|
30863
31270
|
} catch (err) {
|
|
@@ -30934,10 +31341,11 @@ end
|
|
|
30934
31341
|
init_config();
|
|
30935
31342
|
init_log();
|
|
30936
31343
|
init_git();
|
|
30937
|
-
import
|
|
31344
|
+
import fs24 from "fs";
|
|
30938
31345
|
init_resolver();
|
|
31346
|
+
init_stack();
|
|
30939
31347
|
function registerRebaseCommand(program2) {
|
|
30940
|
-
program2.command("rebase <branch>").description("fetch + rebase
|
|
31348
|
+
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
31349
|
const opts = cmd.optsWithGlobals();
|
|
30942
31350
|
const config2 = loadConfig();
|
|
30943
31351
|
const targetRepos = parseRepoFlag(opts.repo);
|
|
@@ -30948,30 +31356,58 @@ function registerRebaseCommand(program2) {
|
|
|
30948
31356
|
repoHeader(repo.name);
|
|
30949
31357
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
30950
31358
|
const wtPath = getWorktreePath(repo, branch);
|
|
30951
|
-
if (!
|
|
31359
|
+
if (!fs24.existsSync(wtPath)) {
|
|
30952
31360
|
stepError("No worktree found", `${wtPath} (skipped)`);
|
|
30953
31361
|
failCount++;
|
|
30954
31362
|
continue;
|
|
30955
31363
|
}
|
|
30956
31364
|
let rebaseStarted = false;
|
|
30957
31365
|
try {
|
|
30958
|
-
const
|
|
30959
|
-
|
|
30960
|
-
const
|
|
30961
|
-
|
|
30962
|
-
|
|
31366
|
+
const metadata = await readStackMetadata(repo.mainPath, opts);
|
|
31367
|
+
const recorded = metadata.branches[branch];
|
|
31368
|
+
const resolvedRemote = !options.onto && !recorded?.explicit ? await resolveBaseRemote(repo.mainPath, mainBranch) : undefined;
|
|
31369
|
+
const defaultBase = resolvedRemote ? `${resolvedRemote}/${mainBranch}` : mainBranch;
|
|
31370
|
+
const baseRef = options.onto || (recorded?.explicit ? recorded.baseRef : defaultBase);
|
|
31371
|
+
const shouldFetchMain = !options.onto && (!recorded || !recorded.explicit);
|
|
31372
|
+
if (shouldFetchMain) {
|
|
31373
|
+
if (!resolvedRemote) {
|
|
31374
|
+
throw new Error(`Could not determine the remote for base branch '${mainBranch}'`);
|
|
31375
|
+
}
|
|
31376
|
+
await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch], opts);
|
|
31377
|
+
const commit = await getLatestCommit(repo.mainPath, defaultBase);
|
|
31378
|
+
stepProgress(`Fetching ${resolvedRemote}/${mainBranch}...`, `${commit.hash} "${commit.subject}"`);
|
|
31379
|
+
} else {
|
|
31380
|
+
stepProgress("Using recorded base", baseRef);
|
|
31381
|
+
}
|
|
31382
|
+
const baseSha = await resolveCommitSha(repo.mainPath, baseRef, opts);
|
|
31383
|
+
stepProgress(`Rebasing ${branch} onto ${baseRef}...`);
|
|
30963
31384
|
rebaseStarted = true;
|
|
30964
|
-
const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--",
|
|
31385
|
+
const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--", baseRef], opts);
|
|
30965
31386
|
if (rebaseOut.includes("is up to date") || rebaseOut.includes("up-to-date")) {
|
|
30966
31387
|
stepSuccess("Up to date", "0 commits replayed");
|
|
30967
31388
|
} else {
|
|
30968
|
-
const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${
|
|
31389
|
+
const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${baseRef}..HEAD`], opts).then((s) => s.trim());
|
|
30969
31390
|
stepSuccess("Rebased", `${count2} commits replayed`);
|
|
30970
31391
|
}
|
|
31392
|
+
if (recorded || options.onto) {
|
|
31393
|
+
const metadataBaseRef = options.onto || (recorded?.explicit ? baseRef : mainBranch);
|
|
31394
|
+
try {
|
|
31395
|
+
await recordStackEntry(repo.mainPath, branch, {
|
|
31396
|
+
baseRef: metadataBaseRef,
|
|
31397
|
+
baseSha,
|
|
31398
|
+
explicit: options.onto ? true : recorded?.explicit ?? true,
|
|
31399
|
+
createdAt: recorded?.createdAt || new Date().toISOString()
|
|
31400
|
+
}, opts);
|
|
31401
|
+
stepSuccess("Base recorded", metadataBaseRef);
|
|
31402
|
+
} catch (err) {
|
|
31403
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31404
|
+
stepWarning("Base metadata not updated", message);
|
|
31405
|
+
}
|
|
31406
|
+
}
|
|
30971
31407
|
successCount++;
|
|
30972
31408
|
} catch (err) {
|
|
30973
31409
|
if (!rebaseStarted) {
|
|
30974
|
-
stepError("Rebase skipped — could not
|
|
31410
|
+
stepError("Rebase skipped — could not resolve base:", err.message.split(`
|
|
30975
31411
|
`)[0] ?? err.message);
|
|
30976
31412
|
failCount++;
|
|
30977
31413
|
continue;
|
|
@@ -31046,8 +31482,8 @@ init_execa();
|
|
|
31046
31482
|
init_config();
|
|
31047
31483
|
init_log();
|
|
31048
31484
|
init_resolver();
|
|
31049
|
-
import
|
|
31050
|
-
import
|
|
31485
|
+
import fs25 from "fs";
|
|
31486
|
+
import path32 from "path";
|
|
31051
31487
|
function registerSyncCommand(program2) {
|
|
31052
31488
|
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
31489
|
const opts = cmd.optsWithGlobals();
|
|
@@ -31058,19 +31494,19 @@ function registerSyncCommand(program2) {
|
|
|
31058
31494
|
for (const repo of repos) {
|
|
31059
31495
|
repoHeader(repo.name);
|
|
31060
31496
|
const wtPath = getWorktreePath(repo, branch);
|
|
31061
|
-
if (!
|
|
31497
|
+
if (!fs25.existsSync(wtPath)) {
|
|
31062
31498
|
stepWarning("No worktree found", `${wtPath} (skipped)`);
|
|
31063
31499
|
continue;
|
|
31064
31500
|
}
|
|
31065
31501
|
try {
|
|
31066
31502
|
if (repo.config.sync_files) {
|
|
31067
31503
|
for (const file2 of repo.config.sync_files) {
|
|
31068
|
-
const src =
|
|
31069
|
-
const dest =
|
|
31070
|
-
if (
|
|
31504
|
+
const src = path32.join(repo.mainPath, file2);
|
|
31505
|
+
const dest = path32.join(wtPath, file2);
|
|
31506
|
+
if (fs25.existsSync(src)) {
|
|
31071
31507
|
if (!opts.dryRun) {
|
|
31072
|
-
|
|
31073
|
-
|
|
31508
|
+
fs25.mkdirSync(path32.dirname(dest), { recursive: true });
|
|
31509
|
+
fs25.copyFileSync(src, dest);
|
|
31074
31510
|
}
|
|
31075
31511
|
stepSuccess(`Synced ${file2}`);
|
|
31076
31512
|
}
|
|
@@ -31120,15 +31556,15 @@ function registerSyncCommand(program2) {
|
|
|
31120
31556
|
process.exit(1);
|
|
31121
31557
|
}
|
|
31122
31558
|
}
|
|
31123
|
-
const wtNodeModules =
|
|
31124
|
-
if (
|
|
31559
|
+
const wtNodeModules = path32.join(wtPath, "node_modules");
|
|
31560
|
+
if (fs25.existsSync(wtNodeModules) && fs25.lstatSync(wtNodeModules).isSymbolicLink()) {
|
|
31125
31561
|
const lockfiles = ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lockb", "bun.lock"];
|
|
31126
31562
|
for (const lock of lockfiles) {
|
|
31127
|
-
const mainLock =
|
|
31128
|
-
const wtLock =
|
|
31129
|
-
if (
|
|
31130
|
-
const mainContent =
|
|
31131
|
-
const wtContent =
|
|
31563
|
+
const mainLock = path32.join(repo.mainPath, lock);
|
|
31564
|
+
const wtLock = path32.join(wtPath, lock);
|
|
31565
|
+
if (fs25.existsSync(mainLock) && fs25.existsSync(wtLock)) {
|
|
31566
|
+
const mainContent = fs25.readFileSync(mainLock);
|
|
31567
|
+
const wtContent = fs25.readFileSync(wtLock);
|
|
31132
31568
|
if (!mainContent.equals(wtContent)) {
|
|
31133
31569
|
stepWarning(`${lock} differs from main — node_modules is symlinked`);
|
|
31134
31570
|
indented(`Run: wtx deps ${branch} --repo ${repo.name} --install`);
|
|
@@ -31157,7 +31593,7 @@ init_config();
|
|
|
31157
31593
|
init_log();
|
|
31158
31594
|
init_resolver();
|
|
31159
31595
|
init_deps();
|
|
31160
|
-
import
|
|
31596
|
+
import fs26 from "fs";
|
|
31161
31597
|
function registerDepsCommand(program2) {
|
|
31162
31598
|
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
31599
|
const globalOpts = program2.opts();
|
|
@@ -31172,16 +31608,29 @@ function registerDepsCommand(program2) {
|
|
|
31172
31608
|
if (!options.json)
|
|
31173
31609
|
repoHeader(repo.name);
|
|
31174
31610
|
if (!branch) {
|
|
31175
|
-
|
|
31176
|
-
|
|
31177
|
-
|
|
31611
|
+
if (options.install) {
|
|
31612
|
+
const ok = await switchToInstall(repo.mainPath, globalOpts);
|
|
31613
|
+
if (!ok)
|
|
31614
|
+
process.exitCode = 1;
|
|
31615
|
+
if (options.json) {
|
|
31616
|
+
jsonResults[repo.name] = detectDepsState(repo.mainPath, repo.mainPath);
|
|
31617
|
+
}
|
|
31618
|
+
} else if (options.symlink) {
|
|
31619
|
+
if (!options.json) {
|
|
31620
|
+
stepWarning("Symlink strategy requires a worktree", "usage: wtx deps <branch> --symlink");
|
|
31621
|
+
}
|
|
31178
31622
|
} else {
|
|
31179
|
-
|
|
31623
|
+
const state = detectDepsState(repo.mainPath, repo.mainPath);
|
|
31624
|
+
if (options.json) {
|
|
31625
|
+
jsonResults[repo.name] = { main: state };
|
|
31626
|
+
} else {
|
|
31627
|
+
info(` Main repo package manager: ${state.packageManager ?? "none detected"}`);
|
|
31628
|
+
}
|
|
31180
31629
|
}
|
|
31181
31630
|
continue;
|
|
31182
31631
|
}
|
|
31183
31632
|
const wtPath = getWorktreePath(repo, branch);
|
|
31184
|
-
if (!
|
|
31633
|
+
if (!fs26.existsSync(wtPath)) {
|
|
31185
31634
|
if (options.json) {
|
|
31186
31635
|
jsonResults[repo.name] = { error: "No worktree found", branch };
|
|
31187
31636
|
} else {
|
|
@@ -31239,7 +31688,7 @@ function registerDepsCommand(program2) {
|
|
|
31239
31688
|
init_config();
|
|
31240
31689
|
init_log();
|
|
31241
31690
|
init_resolver();
|
|
31242
|
-
import
|
|
31691
|
+
import fs27 from "fs";
|
|
31243
31692
|
init_git();
|
|
31244
31693
|
async function resolveMainCheckoutPath(repoCtx, branch) {
|
|
31245
31694
|
try {
|
|
@@ -31264,7 +31713,7 @@ function registerOpenCommand(program2) {
|
|
|
31264
31713
|
for (const repo of repos) {
|
|
31265
31714
|
const wtPath = getWorktreePath(repo, branch);
|
|
31266
31715
|
let targetPath = wtPath;
|
|
31267
|
-
if (!
|
|
31716
|
+
if (!fs27.existsSync(wtPath)) {
|
|
31268
31717
|
const mainCheckoutPath = await resolveMainCheckoutPath(repo, branch);
|
|
31269
31718
|
if (!mainCheckoutPath) {
|
|
31270
31719
|
continue;
|
|
@@ -31294,14 +31743,14 @@ init_config();
|
|
|
31294
31743
|
init_log();
|
|
31295
31744
|
init_git();
|
|
31296
31745
|
init_resolver();
|
|
31297
|
-
import
|
|
31746
|
+
import path34 from "path";
|
|
31298
31747
|
|
|
31299
31748
|
// src/lib/rename-worktree.ts
|
|
31300
31749
|
init_git();
|
|
31301
31750
|
init_resolver();
|
|
31302
31751
|
init_path_safety();
|
|
31303
|
-
import
|
|
31304
|
-
import
|
|
31752
|
+
import fs28 from "fs";
|
|
31753
|
+
import path33 from "path";
|
|
31305
31754
|
async function getUpstream(wtPath) {
|
|
31306
31755
|
try {
|
|
31307
31756
|
const out = await gitExec(["-C", wtPath, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {});
|
|
@@ -31321,7 +31770,7 @@ async function planRename(repo, oldBranch, newBranch, opts) {
|
|
|
31321
31770
|
throw new Error(`Worktree '${oldBranch}' is locked — unlock it before renaming`);
|
|
31322
31771
|
}
|
|
31323
31772
|
const newPath = `${repo.wtRoot}/${newBranch}`;
|
|
31324
|
-
if (
|
|
31773
|
+
if (fs28.existsSync(newPath)) {
|
|
31325
31774
|
throw new Error(`Target path already exists: ${newPath}`);
|
|
31326
31775
|
}
|
|
31327
31776
|
if (!opts.dryRun && await localBranchExists(repo.mainPath, newBranch, opts)) {
|
|
@@ -31345,7 +31794,7 @@ async function renameWorktree(params) {
|
|
|
31345
31794
|
}
|
|
31346
31795
|
await gitExec(["-C", planned.worktreePath, "branch", "-m", oldBranch, newBranch], opts);
|
|
31347
31796
|
try {
|
|
31348
|
-
|
|
31797
|
+
fs28.mkdirSync(path33.dirname(planned.newPath), { recursive: true });
|
|
31349
31798
|
await gitExec(["-C", repo.mainPath, "worktree", "move", planned.worktreePath, planned.newPath], opts);
|
|
31350
31799
|
} catch (err) {
|
|
31351
31800
|
try {
|
|
@@ -31369,6 +31818,7 @@ async function renameWorktree(params) {
|
|
|
31369
31818
|
}
|
|
31370
31819
|
|
|
31371
31820
|
// src/commands/rename.ts
|
|
31821
|
+
init_stack();
|
|
31372
31822
|
function registerRenameCommand(program2) {
|
|
31373
31823
|
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
31824
|
const globalOpts = program2.opts();
|
|
@@ -31406,12 +31856,19 @@ function registerRenameCommand(program2) {
|
|
|
31406
31856
|
stepProgress(`Renaming ${oldBranch} → ${newBranch}...`);
|
|
31407
31857
|
const outcome = await renameWorktree({ repo, oldBranch, newBranch, opts: globalOpts });
|
|
31408
31858
|
for (const dir of outcome.cleanedDirs) {
|
|
31409
|
-
stepSuccess("Cleaned up empty directory",
|
|
31859
|
+
stepSuccess("Cleaned up empty directory", path34.relative(repo.wtRoot, dir) + "/");
|
|
31410
31860
|
}
|
|
31411
31861
|
stepSuccess("Renamed", `${outcome.oldPath} → ${outcome.newPath}`);
|
|
31412
31862
|
if (outcome.upstream) {
|
|
31413
31863
|
indented(`Upstream still tracks '${outcome.upstream}' — after pushing run: git push -u origin ${newBranch}`);
|
|
31414
31864
|
}
|
|
31865
|
+
try {
|
|
31866
|
+
await renameStackEntry(repo.mainPath, oldBranch, newBranch, globalOpts);
|
|
31867
|
+
stepSuccess("Updated stack metadata", newBranch);
|
|
31868
|
+
} catch (err) {
|
|
31869
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31870
|
+
stepWarning("Stack metadata not updated", message);
|
|
31871
|
+
}
|
|
31415
31872
|
summary(`Done — renamed ${oldBranch} to ${newBranch}`);
|
|
31416
31873
|
} catch (err) {
|
|
31417
31874
|
stepError("Rename failed", err.message);
|
|
@@ -31428,8 +31885,9 @@ init_resolver();
|
|
|
31428
31885
|
init_deps();
|
|
31429
31886
|
init_forge();
|
|
31430
31887
|
init_types2();
|
|
31431
|
-
import
|
|
31888
|
+
import fs29 from "fs";
|
|
31432
31889
|
init_owner();
|
|
31890
|
+
init_stack();
|
|
31433
31891
|
init_source();
|
|
31434
31892
|
function buildStatusJson(item) {
|
|
31435
31893
|
const entry = {
|
|
@@ -31446,11 +31904,14 @@ function buildStatusJson(item) {
|
|
|
31446
31904
|
entry.behind = item.behind;
|
|
31447
31905
|
entry.deps = item.deps;
|
|
31448
31906
|
if (item.prInfo) {
|
|
31449
|
-
|
|
31907
|
+
const prEntry = {
|
|
31450
31908
|
number: item.prInfo.number,
|
|
31451
31909
|
state: item.prInfo.state,
|
|
31452
31910
|
url: item.prInfo.url
|
|
31453
31911
|
};
|
|
31912
|
+
if (item.prInfo.baseRefName)
|
|
31913
|
+
prEntry.base = item.prInfo.baseRefName;
|
|
31914
|
+
entry.pr = prEntry;
|
|
31454
31915
|
}
|
|
31455
31916
|
if (item.ownership && !item.ownership.mine && item.ownership.author) {
|
|
31456
31917
|
entry.owner = item.ownership.author;
|
|
@@ -31458,10 +31919,14 @@ function buildStatusJson(item) {
|
|
|
31458
31919
|
if (item.rebase) {
|
|
31459
31920
|
entry.rebase = item.rebase;
|
|
31460
31921
|
}
|
|
31922
|
+
if (item.base)
|
|
31923
|
+
entry.base = item.base;
|
|
31924
|
+
if (item.baseChanged)
|
|
31925
|
+
entry.baseChanged = true;
|
|
31461
31926
|
return entry;
|
|
31462
31927
|
}
|
|
31463
31928
|
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) => {
|
|
31929
|
+
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
31930
|
const globalOpts = program2.opts();
|
|
31466
31931
|
const config2 = loadConfig();
|
|
31467
31932
|
const repoFilter = parseRepoFlag(options.repo);
|
|
@@ -31470,17 +31935,33 @@ function registerStatusCommand(program2) {
|
|
|
31470
31935
|
const jsonOutputs = [];
|
|
31471
31936
|
for (const repo of repos) {
|
|
31472
31937
|
const wtPath = getWorktreePath(repo, branch);
|
|
31473
|
-
if (!
|
|
31938
|
+
if (!fs29.existsSync(wtPath)) {
|
|
31474
31939
|
continue;
|
|
31475
31940
|
}
|
|
31476
31941
|
found++;
|
|
31477
31942
|
const dirtyFiles = await getDirtyFiles(wtPath);
|
|
31478
31943
|
let ahead = null;
|
|
31479
31944
|
let behind = null;
|
|
31945
|
+
let baseRef;
|
|
31946
|
+
let baseChanged = false;
|
|
31947
|
+
let usingStackBase = false;
|
|
31480
31948
|
try {
|
|
31481
31949
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
31482
|
-
const
|
|
31483
|
-
const
|
|
31950
|
+
const metadata = await readStackMetadata(repo.mainPath, globalOpts);
|
|
31951
|
+
const recorded = metadata.branches[branch];
|
|
31952
|
+
const resolvedRemote = !options.base && !recorded?.explicit ? await resolveBaseRemote(repo.mainPath, mainBranch) : undefined;
|
|
31953
|
+
const defaultBase = resolvedRemote ? `${resolvedRemote}/${mainBranch}` : mainBranch;
|
|
31954
|
+
baseRef = options.base || (recorded?.explicit ? recorded.baseRef : defaultBase);
|
|
31955
|
+
usingStackBase = Boolean(options.base || recorded?.explicit);
|
|
31956
|
+
if (recorded?.explicit && !options.base && !globalOpts.dryRun) {
|
|
31957
|
+
try {
|
|
31958
|
+
const currentBaseSha = await resolveCommitSha(repo.mainPath, recorded.baseRef, globalOpts);
|
|
31959
|
+
baseChanged = currentBaseSha !== recorded.baseSha;
|
|
31960
|
+
} catch {
|
|
31961
|
+
baseChanged = true;
|
|
31962
|
+
}
|
|
31963
|
+
}
|
|
31964
|
+
const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${baseRef}...HEAD`], { verbose: globalOpts.verbose });
|
|
31484
31965
|
const parts = countOutput.trim().split(/\s+/);
|
|
31485
31966
|
behind = parts[0] ? parseInt(parts[0], 10) : null;
|
|
31486
31967
|
ahead = parts[1] ? parseInt(parts[1], 10) : null;
|
|
@@ -31519,7 +32000,9 @@ function registerStatusCommand(program2) {
|
|
|
31519
32000
|
prInfo,
|
|
31520
32001
|
ownership,
|
|
31521
32002
|
deps: depsState,
|
|
31522
|
-
rebase: rebaseStatus
|
|
32003
|
+
rebase: rebaseStatus,
|
|
32004
|
+
base: usingStackBase ? baseRef : undefined,
|
|
32005
|
+
baseChanged
|
|
31523
32006
|
}));
|
|
31524
32007
|
continue;
|
|
31525
32008
|
}
|
|
@@ -31533,10 +32016,16 @@ function registerStatusCommand(program2) {
|
|
|
31533
32016
|
indented(` ${f}`);
|
|
31534
32017
|
}
|
|
31535
32018
|
}
|
|
32019
|
+
if (usingStackBase && baseRef) {
|
|
32020
|
+
info(` Base: ${baseRef}`);
|
|
32021
|
+
if (baseChanged) {
|
|
32022
|
+
info(` Base state: moved since stack entry`);
|
|
32023
|
+
}
|
|
32024
|
+
}
|
|
31536
32025
|
if (ahead !== null && behind !== null) {
|
|
31537
|
-
info(` vs main: ${ahead} ahead, ${behind} behind`);
|
|
32026
|
+
info(` ${usingStackBase ? "vs base" : "vs main"}: ${ahead} ahead, ${behind} behind`);
|
|
31538
32027
|
} else {
|
|
31539
|
-
info(` vs main: unknown`);
|
|
32028
|
+
info(` ${usingStackBase ? "vs base" : "vs main"}: unknown`);
|
|
31540
32029
|
}
|
|
31541
32030
|
if (prInfo) {
|
|
31542
32031
|
const display = derivePrDisplay(prInfo);
|
|
@@ -31583,6 +32072,7 @@ init_git();
|
|
|
31583
32072
|
init_resolver();
|
|
31584
32073
|
init_forge();
|
|
31585
32074
|
init_owner();
|
|
32075
|
+
init_stack();
|
|
31586
32076
|
init_types2();
|
|
31587
32077
|
var ATTENTION_STATES = new Set([
|
|
31588
32078
|
PR_DISPLAY_STATES.CONFLICTED,
|
|
@@ -31614,6 +32104,7 @@ async function collectPrRows(repos, config2, verboseFlag) {
|
|
|
31614
32104
|
continue;
|
|
31615
32105
|
try {
|
|
31616
32106
|
const worktrees = await getWorktreeList(repo.mainPath);
|
|
32107
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: verboseFlag, dryRun: false });
|
|
31617
32108
|
const branches = worktrees.filter((wt) => wt.path !== repo.mainPath && wt.branch).map((wt) => wt.branch);
|
|
31618
32109
|
if (branches.length === 0)
|
|
31619
32110
|
continue;
|
|
@@ -31643,7 +32134,8 @@ async function collectPrRows(repos, config2, verboseFlag) {
|
|
|
31643
32134
|
unresolvedThreads: pr.unresolvedThreads,
|
|
31644
32135
|
updatedAt: pr.updatedAt,
|
|
31645
32136
|
authorLogin: pr.authorLogin ?? null,
|
|
31646
|
-
ownership
|
|
32137
|
+
ownership,
|
|
32138
|
+
baseRef: pr.baseRefName ?? stackMetadata.branches[branch]?.baseRef ?? null
|
|
31647
32139
|
});
|
|
31648
32140
|
}
|
|
31649
32141
|
} catch (err) {
|
|
@@ -31669,8 +32161,9 @@ function renderTable(rows) {
|
|
|
31669
32161
|
const threads = row.unresolvedThreads > 0 ? `${row.unresolvedThreads} thread${row.unresolvedThreads > 1 ? "s" : ""}` : null;
|
|
31670
32162
|
const details = [row.checksSummary, threads].filter(Boolean).join(" · ");
|
|
31671
32163
|
const detailSuffix = details ? ` ${details}` : "";
|
|
32164
|
+
const baseSuffix = row.baseRef ? ` → ${row.baseRef}` : "";
|
|
31672
32165
|
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)}`);
|
|
32166
|
+
info(` #${row.prNumber} ${paddedBranch} ${renderDisplayState(row.prDisplay)}${baseSuffix}${detailSuffix}${authorTag} ${formatRelativeTime(row.updatedAt)} ${source_default.dim(row.url)}`);
|
|
31674
32167
|
}
|
|
31675
32168
|
}
|
|
31676
32169
|
}
|
|
@@ -31683,7 +32176,8 @@ function toJsonOutput(rows) {
|
|
|
31683
32176
|
awaitingReview: row.prDisplay.awaitingReview,
|
|
31684
32177
|
approved: row.prDisplay.approved,
|
|
31685
32178
|
prNumber: row.prNumber,
|
|
31686
|
-
author: row.authorLogin
|
|
32179
|
+
author: row.authorLogin,
|
|
32180
|
+
base: row.baseRef
|
|
31687
32181
|
}));
|
|
31688
32182
|
}
|
|
31689
32183
|
function registerPrsCommand(program2) {
|
|
@@ -31741,8 +32235,8 @@ function registerPrsCommand(program2) {
|
|
|
31741
32235
|
|
|
31742
32236
|
// src/commands/skill.ts
|
|
31743
32237
|
init_log();
|
|
31744
|
-
import
|
|
31745
|
-
import
|
|
32238
|
+
import fs30 from "fs";
|
|
32239
|
+
import path35 from "path";
|
|
31746
32240
|
import { URL as URL2 } from "url";
|
|
31747
32241
|
var __dirname2 = new URL2(".", import.meta.url).pathname;
|
|
31748
32242
|
var COMMON_MARKDOWN = `# wtx — Worktree Manager
|
|
@@ -31771,8 +32265,12 @@ Lists all worktrees across repositories.
|
|
|
31771
32265
|
Shows git statuses for all worktrees.
|
|
31772
32266
|
|
|
31773
32267
|
### \`wtx rebase <branch>\`
|
|
31774
|
-
Fetches the main branch
|
|
31775
|
-
- **Flags**: \`-r, --repo <repos
|
|
32268
|
+
Fetches the configured main branch for an independent worktree, or rebases onto its recorded base for a stacked worktree.
|
|
32269
|
+
- **Flags**: \`-r, --repo <repos...>\`, \`--onto <ref>\` to override the base.
|
|
32270
|
+
|
|
32271
|
+
### \`wtx stack <branch>\`
|
|
32272
|
+
Shows the recorded parent and descendant branches for a worktree.
|
|
32273
|
+
- **Flags**: \`-r, --repo <repos...>\`, \`--json\`
|
|
31776
32274
|
|
|
31777
32275
|
### \`wtx sync <branch>\`
|
|
31778
32276
|
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 +32335,23 @@ Hooks (\`post_create\` and \`post_sync\`) support the following template variabl
|
|
|
31837
32335
|
\`\`\`bash
|
|
31838
32336
|
wtx rebase feature-xyz
|
|
31839
32337
|
\`\`\`
|
|
31840
|
-
*Fetches the latest main branch
|
|
32338
|
+
*Fetches the latest main branch for independent work, or uses the recorded parent for a stacked branch.*
|
|
32339
|
+
|
|
32340
|
+
3. **Creating a stacked branch**:
|
|
32341
|
+
\`\`\`bash
|
|
32342
|
+
wtx create feature-api
|
|
32343
|
+
wtx create feature-ui --base feature-api
|
|
32344
|
+
wtx stack feature-ui
|
|
32345
|
+
\`\`\`
|
|
32346
|
+
*Open the child PR against \`feature-api\`; after the parent merges, retarget the child to main before rebasing it onto main.*
|
|
31841
32347
|
|
|
31842
|
-
|
|
32348
|
+
4. **Syncing environment variables**:
|
|
31843
32349
|
If the \`.env\` file in the main checkout was updated, run:
|
|
31844
32350
|
\`\`\`bash
|
|
31845
32351
|
wtx sync feature-xyz
|
|
31846
32352
|
\`\`\`
|
|
31847
32353
|
|
|
31848
|
-
|
|
32354
|
+
5. **Managing node_modules dependencies**:
|
|
31849
32355
|
If a worktree requires different dependencies than the main branch (e.g. you're testing an upgrade):
|
|
31850
32356
|
\`\`\`bash
|
|
31851
32357
|
wtx deps feature-xyz --install
|
|
@@ -31884,9 +32390,9 @@ function registerSkillCommand(program2) {
|
|
|
31884
32390
|
error51(`Unknown platform: ${platform2}`);
|
|
31885
32391
|
process.exit(1);
|
|
31886
32392
|
}
|
|
31887
|
-
let projectRoot =
|
|
31888
|
-
let skillPath =
|
|
31889
|
-
if (
|
|
32393
|
+
let projectRoot = path35.resolve(__dirname2, "..", "..");
|
|
32394
|
+
let skillPath = path35.join(projectRoot, "skills", `${platform2}.md`);
|
|
32395
|
+
if (fs30.existsSync(skillPath)) {
|
|
31890
32396
|
process.stdout.write(skillPath + `
|
|
31891
32397
|
`);
|
|
31892
32398
|
} else {
|
|
@@ -31932,7 +32438,8 @@ init_git();
|
|
|
31932
32438
|
init_deps();
|
|
31933
32439
|
import readline3 from "readline";
|
|
31934
32440
|
init_path_safety();
|
|
31935
|
-
|
|
32441
|
+
init_stack();
|
|
32442
|
+
import fs32 from "fs";
|
|
31936
32443
|
async function runMcpServer(opts = {}) {
|
|
31937
32444
|
const input = opts.input ?? process.stdin;
|
|
31938
32445
|
const output = opts.output ?? process.stdout;
|
|
@@ -32025,7 +32532,7 @@ async function handleRequest(req, send, config2, opts) {
|
|
|
32025
32532
|
description: "Get status of a specific worktree",
|
|
32026
32533
|
inputSchema: {
|
|
32027
32534
|
type: "object",
|
|
32028
|
-
properties: { repo: { type: "string" }, branch: { type: "string" } },
|
|
32535
|
+
properties: { repo: { type: "string" }, branch: { type: "string" }, base: { type: "string" } },
|
|
32029
32536
|
required: ["repo", "branch"]
|
|
32030
32537
|
}
|
|
32031
32538
|
},
|
|
@@ -32049,10 +32556,10 @@ async function handleRequest(req, send, config2, opts) {
|
|
|
32049
32556
|
},
|
|
32050
32557
|
{
|
|
32051
32558
|
name: "rebase_worktree",
|
|
32052
|
-
description: "Rebase a worktree against
|
|
32559
|
+
description: "Rebase a worktree against its recorded base or an explicit ref",
|
|
32053
32560
|
inputSchema: {
|
|
32054
32561
|
type: "object",
|
|
32055
|
-
properties: { repo: { type: "string" }, branch: { type: "string" } },
|
|
32562
|
+
properties: { repo: { type: "string" }, branch: { type: "string" }, onto: { type: "string" } },
|
|
32056
32563
|
required: ["repo", "branch"]
|
|
32057
32564
|
}
|
|
32058
32565
|
}
|
|
@@ -32111,16 +32618,18 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32111
32618
|
const items = [];
|
|
32112
32619
|
for (const repo of repos) {
|
|
32113
32620
|
const wts = await getWorktreeList(repo.mainPath);
|
|
32621
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
|
|
32114
32622
|
for (const wt of wts) {
|
|
32115
32623
|
if (!wt.path.startsWith(repo.wtRoot))
|
|
32116
32624
|
continue;
|
|
32117
|
-
const dirtyCount =
|
|
32625
|
+
const dirtyCount = fs32.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
|
|
32118
32626
|
items.push({
|
|
32119
32627
|
repo: repo.name,
|
|
32120
32628
|
branch: wt.branch || null,
|
|
32121
32629
|
path: wt.path,
|
|
32122
32630
|
sha: wt.commit ? wt.commit.substring(0, 7) : null,
|
|
32123
|
-
dirtyFiles: dirtyCount
|
|
32631
|
+
dirtyFiles: dirtyCount,
|
|
32632
|
+
base: wt.branch && stackMetadata.branches[wt.branch]?.explicit ? stackMetadata.branches[wt.branch]?.baseRef : null
|
|
32124
32633
|
});
|
|
32125
32634
|
}
|
|
32126
32635
|
}
|
|
@@ -32133,18 +32642,27 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32133
32642
|
if (!validateSafeBranchName(args.branch)) {
|
|
32134
32643
|
throw { isToolError: true, message: "unsafe branch name" };
|
|
32135
32644
|
}
|
|
32645
|
+
if (args.base !== undefined && typeof args.base !== "string") {
|
|
32646
|
+
throw { isSchemaError: true, message: "base must be a string" };
|
|
32647
|
+
}
|
|
32648
|
+
if (typeof args.base === "string" && !validateSafeBranchName(args.base)) {
|
|
32649
|
+
throw { isToolError: true, message: "unsafe base ref" };
|
|
32650
|
+
}
|
|
32136
32651
|
const repo = resolveRepos(config2, [args.repo])[0];
|
|
32137
32652
|
const wtPath = getWorktreePath(repo, args.branch);
|
|
32138
|
-
if (!
|
|
32653
|
+
if (!fs32.existsSync(wtPath)) {
|
|
32139
32654
|
throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
|
|
32140
32655
|
}
|
|
32141
32656
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
32142
32657
|
const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
|
|
32658
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
|
|
32659
|
+
const recorded = stackMetadata.branches[args.branch];
|
|
32660
|
+
const baseRef = typeof args.base === "string" ? args.base : recorded?.explicit ? recorded.baseRef : `${resolvedRemote}/${mainBranch}`;
|
|
32143
32661
|
const dirtyFiles = await getDirtyFiles(wtPath);
|
|
32144
32662
|
let ahead = null;
|
|
32145
32663
|
let behind = null;
|
|
32146
32664
|
try {
|
|
32147
|
-
const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${
|
|
32665
|
+
const countOutput = await gitExec(["-C", wtPath, "rev-list", "--left-right", "--count", `${baseRef}...HEAD`]);
|
|
32148
32666
|
const parts = countOutput.trim().split(/\s+/);
|
|
32149
32667
|
behind = parts[0] ? parseInt(parts[0], 10) : null;
|
|
32150
32668
|
ahead = parts[1] ? parseInt(parts[1], 10) : null;
|
|
@@ -32160,6 +32678,7 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32160
32678
|
dirtyCount: dirtyFiles.length,
|
|
32161
32679
|
ahead,
|
|
32162
32680
|
behind,
|
|
32681
|
+
base: recorded?.explicit || typeof args.base === "string" ? baseRef : null,
|
|
32163
32682
|
depsStrategy: depsState.strategy
|
|
32164
32683
|
})
|
|
32165
32684
|
}]
|
|
@@ -32187,9 +32706,11 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32187
32706
|
}
|
|
32188
32707
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
32189
32708
|
const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
|
|
32709
|
+
const baseRef = args.base || `${resolvedRemote}/${mainBranch}`;
|
|
32190
32710
|
if (repo.config.fetch_main_on_create !== false) {
|
|
32191
32711
|
await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch]);
|
|
32192
32712
|
}
|
|
32713
|
+
const baseSha = await resolveCommitSha(repo.mainPath, baseRef, { verbose: _opts.verbose === true, dryRun: false });
|
|
32193
32714
|
const localExists = await localBranchExists(repo.mainPath, args.branch, { verbose: false, dryRun: false });
|
|
32194
32715
|
const remoteExists = await branchExistsOnRemote(repo.mainPath, args.branch, { verbose: false, dryRun: false }, resolvedRemote);
|
|
32195
32716
|
const localSha = localExists ? await getLocalBranchSha(repo.mainPath, args.branch, { verbose: false, dryRun: false }) : null;
|
|
@@ -32207,6 +32728,14 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32207
32728
|
gitArgs.push(wtPath, args.branch);
|
|
32208
32729
|
}
|
|
32209
32730
|
await gitExec(gitArgs);
|
|
32731
|
+
if (resolution.kind === "create-new") {
|
|
32732
|
+
await recordStackEntry(repo.mainPath, args.branch, {
|
|
32733
|
+
baseRef: args.base || mainBranch,
|
|
32734
|
+
baseSha,
|
|
32735
|
+
explicit: args.base !== undefined,
|
|
32736
|
+
createdAt: new Date().toISOString()
|
|
32737
|
+
}, { verbose: _opts.verbose === true, dryRun: false });
|
|
32738
|
+
}
|
|
32210
32739
|
return {
|
|
32211
32740
|
content: [{ type: "text", text: JSON.stringify({ path: wtPath }) }]
|
|
32212
32741
|
};
|
|
@@ -32235,7 +32764,12 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32235
32764
|
throw { isToolError: true, message: `Worktree for ${args.branch} is not registered` };
|
|
32236
32765
|
}
|
|
32237
32766
|
const wtPath = target.path;
|
|
32238
|
-
|
|
32767
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
|
|
32768
|
+
const children = getStackChildren(stackMetadata, args.branch);
|
|
32769
|
+
if (children.length > 0 && args.force !== true) {
|
|
32770
|
+
throw { isToolError: true, message: `Branch has dependent worktrees: ${children.join(", ")}. Use force:true to override.` };
|
|
32771
|
+
}
|
|
32772
|
+
if (!args.force && fs32.existsSync(wtPath)) {
|
|
32239
32773
|
const dirty = await getDirtyFiles(wtPath);
|
|
32240
32774
|
if (dirty.length > 0) {
|
|
32241
32775
|
throw { isToolError: true, message: `Worktree is dirty. Use force:true to remove it.` };
|
|
@@ -32243,6 +32777,7 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32243
32777
|
}
|
|
32244
32778
|
await gitExec(["-C", repo.mainPath, "worktree", "remove", args.force ? "--force" : "", wtPath].filter(Boolean));
|
|
32245
32779
|
cleanupEmptyParents(repo.wtRoot, repo.mainPath, wtPath);
|
|
32780
|
+
await removeStackEntry(repo.mainPath, args.branch, { verbose: _opts.verbose === true, dryRun: false });
|
|
32246
32781
|
return {
|
|
32247
32782
|
content: [{ type: "text", text: JSON.stringify({ removed: true }) }]
|
|
32248
32783
|
};
|
|
@@ -32253,18 +32788,46 @@ async function handleToolCall(name, args, config2, _opts) {
|
|
|
32253
32788
|
}
|
|
32254
32789
|
const repo = resolveRepos(config2, [args.repo])[0];
|
|
32255
32790
|
const wtPath = getWorktreePath(repo, args.branch);
|
|
32256
|
-
if (!
|
|
32791
|
+
if (!fs32.existsSync(wtPath)) {
|
|
32257
32792
|
throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
|
|
32258
32793
|
}
|
|
32259
32794
|
const mainBranch = await resolveMainBranch(repo, config2);
|
|
32260
32795
|
const resolvedRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
|
|
32261
|
-
|
|
32796
|
+
if (args.onto !== undefined && typeof args.onto !== "string") {
|
|
32797
|
+
throw { isSchemaError: true, message: "onto must be a string" };
|
|
32798
|
+
}
|
|
32799
|
+
if (typeof args.onto === "string" && !validateSafeBranchName(args.onto)) {
|
|
32800
|
+
throw { isToolError: true, message: "unsafe base ref" };
|
|
32801
|
+
}
|
|
32802
|
+
const stackMetadata = await readStackMetadata(repo.mainPath, { verbose: _opts.verbose === true, dryRun: false });
|
|
32803
|
+
const recorded = stackMetadata.branches[args.branch];
|
|
32804
|
+
const baseRef = args.onto || (recorded?.explicit ? recorded.baseRef : `${resolvedRemote}/${mainBranch}`);
|
|
32805
|
+
if (!args.onto && (!recorded || !recorded.explicit)) {
|
|
32806
|
+
await gitExec(["-C", repo.mainPath, "fetch", resolvedRemote, "--", mainBranch]);
|
|
32807
|
+
}
|
|
32808
|
+
const baseSha = await resolveCommitSha(repo.mainPath, baseRef, { verbose: _opts.verbose === true, dryRun: false });
|
|
32262
32809
|
try {
|
|
32263
|
-
const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--",
|
|
32810
|
+
const rebaseOut = await gitExec(["-C", wtPath, "rebase", "--", baseRef]);
|
|
32264
32811
|
if (rebaseOut.includes("is up to date") || rebaseOut.includes("up-to-date")) {
|
|
32812
|
+
if (recorded || args.onto) {
|
|
32813
|
+
await recordStackEntry(repo.mainPath, args.branch, {
|
|
32814
|
+
baseRef: args.onto || (recorded?.explicit ? baseRef : mainBranch),
|
|
32815
|
+
baseSha,
|
|
32816
|
+
explicit: args.onto ? true : recorded?.explicit ?? true,
|
|
32817
|
+
createdAt: recorded?.createdAt || new Date().toISOString()
|
|
32818
|
+
}, { verbose: _opts.verbose === true, dryRun: false });
|
|
32819
|
+
}
|
|
32265
32820
|
return { content: [{ type: "text", text: JSON.stringify({ status: "up-to-date" }) }] };
|
|
32266
32821
|
} else {
|
|
32267
|
-
const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${
|
|
32822
|
+
const count2 = await gitExec(["-C", wtPath, "rev-list", "--count", `${baseRef}..HEAD`]).then((s) => s.trim());
|
|
32823
|
+
if (recorded || args.onto) {
|
|
32824
|
+
await recordStackEntry(repo.mainPath, args.branch, {
|
|
32825
|
+
baseRef: args.onto || (recorded?.explicit ? baseRef : mainBranch),
|
|
32826
|
+
baseSha,
|
|
32827
|
+
explicit: args.onto ? true : recorded?.explicit ?? true,
|
|
32828
|
+
createdAt: recorded?.createdAt || new Date().toISOString()
|
|
32829
|
+
}, { verbose: _opts.verbose === true, dryRun: false });
|
|
32830
|
+
}
|
|
32268
32831
|
return { content: [{ type: "text", text: JSON.stringify({ status: "rebased", commits: count2 }) }] };
|
|
32269
32832
|
}
|
|
32270
32833
|
} catch (err) {
|
|
@@ -32354,12 +32917,100 @@ function registerHistoryCommand(program2) {
|
|
|
32354
32917
|
});
|
|
32355
32918
|
}
|
|
32356
32919
|
|
|
32920
|
+
// src/commands/stack.ts
|
|
32921
|
+
init_config();
|
|
32922
|
+
init_git();
|
|
32923
|
+
init_resolver();
|
|
32924
|
+
init_stack();
|
|
32925
|
+
init_log();
|
|
32926
|
+
function nodeFor(branch, metadata, worktrees) {
|
|
32927
|
+
const entry = metadata.branches[branch];
|
|
32928
|
+
const worktree = worktrees.find((wt) => wt.branch === branch)?.path ?? null;
|
|
32929
|
+
return {
|
|
32930
|
+
branch,
|
|
32931
|
+
base: entry?.baseRef ?? null,
|
|
32932
|
+
explicit: entry?.explicit ?? false,
|
|
32933
|
+
baseSha: entry?.baseSha ?? null,
|
|
32934
|
+
worktree
|
|
32935
|
+
};
|
|
32936
|
+
}
|
|
32937
|
+
function collectDescendants(metadata, branch, worktrees, seen) {
|
|
32938
|
+
const nodes = [];
|
|
32939
|
+
for (const child of getStackChildren(metadata, branch)) {
|
|
32940
|
+
if (seen.has(child))
|
|
32941
|
+
continue;
|
|
32942
|
+
seen.add(child);
|
|
32943
|
+
nodes.push(nodeFor(child, metadata, worktrees));
|
|
32944
|
+
nodes.push(...collectDescendants(metadata, child, worktrees, seen));
|
|
32945
|
+
}
|
|
32946
|
+
return nodes;
|
|
32947
|
+
}
|
|
32948
|
+
function renderNode(node, prefix) {
|
|
32949
|
+
const base2 = node.base ? ` base ${node.base}` : " base not recorded";
|
|
32950
|
+
const location = node.worktree ? ` ${node.worktree}` : " no local worktree";
|
|
32951
|
+
info(` ${prefix}${node.branch}`);
|
|
32952
|
+
indented(`${" ".repeat(prefix.length)}${base2} · ${location}`);
|
|
32953
|
+
}
|
|
32954
|
+
function registerStackCommand(program2) {
|
|
32955
|
+
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) => {
|
|
32956
|
+
const globalOpts = program2.opts();
|
|
32957
|
+
if (!validateSafeBranchName(branch)) {
|
|
32958
|
+
stepError(`Invalid branch name: '${branch}'`);
|
|
32959
|
+
process.exit(1);
|
|
32960
|
+
}
|
|
32961
|
+
const config2 = loadConfig();
|
|
32962
|
+
const repoFilter = parseRepoFlag(options.repo);
|
|
32963
|
+
const repos = resolveRepos(config2, repoFilter);
|
|
32964
|
+
const jsonResults = [];
|
|
32965
|
+
let successCount = 0;
|
|
32966
|
+
for (const repo of repos) {
|
|
32967
|
+
try {
|
|
32968
|
+
const metadata = await readStackMetadata(repo.mainPath, globalOpts);
|
|
32969
|
+
const worktrees = await getWorktreeList(repo.mainPath);
|
|
32970
|
+
const ancestors = getStackAncestors(metadata, branch);
|
|
32971
|
+
const nodes = ancestors.map((item) => nodeFor(item, metadata, worktrees));
|
|
32972
|
+
const descendants = collectDescendants(metadata, branch, worktrees, new Set(ancestors));
|
|
32973
|
+
const allNodes = [...nodes, ...descendants];
|
|
32974
|
+
const displayNodes = buildStackHierarchy(allNodes, (node) => node.branch, (node) => node.base ?? undefined, (a2, b) => a2.branch.localeCompare(b.branch));
|
|
32975
|
+
if (options.json) {
|
|
32976
|
+
jsonResults.push({ repo: repo.name, branch, nodes: allNodes });
|
|
32977
|
+
} else {
|
|
32978
|
+
repoHeader(repo.name);
|
|
32979
|
+
info(` Stack for ${branch}`);
|
|
32980
|
+
if (allNodes.length === 1 && !metadata.branches[branch]) {
|
|
32981
|
+
const mainBranch = await resolveMainBranch(repo, config2);
|
|
32982
|
+
const defaultRemote = await resolveBaseRemote(repo.mainPath, mainBranch);
|
|
32983
|
+
const defaultBase = `${defaultRemote}/${mainBranch}`;
|
|
32984
|
+
indented(`No recorded parent; default base is ${defaultBase}`);
|
|
32985
|
+
}
|
|
32986
|
+
displayNodes.forEach(({ item, prefix }) => renderNode(item, prefix));
|
|
32987
|
+
}
|
|
32988
|
+
successCount++;
|
|
32989
|
+
} catch (err) {
|
|
32990
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32991
|
+
if (options.json) {
|
|
32992
|
+
console.error(JSON.stringify({ repo: repo.name, error: message }));
|
|
32993
|
+
} else {
|
|
32994
|
+
stepError("Failed to read stack", `${repo.name}: ${message}`);
|
|
32995
|
+
}
|
|
32996
|
+
}
|
|
32997
|
+
}
|
|
32998
|
+
if (options.json) {
|
|
32999
|
+
console.log(JSON.stringify(jsonResults.length === 1 ? jsonResults[0] : jsonResults, null, 2));
|
|
33000
|
+
} else if (successCount === 0) {
|
|
33001
|
+
summaryWarning("No stacks found");
|
|
33002
|
+
} else {
|
|
33003
|
+
summary(`Done — ${successCount} repo${successCount > 1 ? "s" : ""} checked`);
|
|
33004
|
+
}
|
|
33005
|
+
});
|
|
33006
|
+
}
|
|
33007
|
+
|
|
32357
33008
|
// src/index.ts
|
|
32358
33009
|
init_config();
|
|
32359
33010
|
init_resolver();
|
|
32360
33011
|
init_log();
|
|
32361
33012
|
init_history();
|
|
32362
|
-
import
|
|
33013
|
+
import fs33 from "fs";
|
|
32363
33014
|
var program2 = new Command;
|
|
32364
33015
|
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
33016
|
var MUTATING_COMMANDS = new Set([
|
|
@@ -32451,6 +33102,7 @@ registerSkillCommand(program2);
|
|
|
32451
33102
|
registerTerminalCommand(program2);
|
|
32452
33103
|
registerMcpCommand(program2);
|
|
32453
33104
|
registerHistoryCommand(program2);
|
|
33105
|
+
registerStackCommand(program2);
|
|
32454
33106
|
program2.command("_resolve-path <repo> <branch>", { hidden: true }).description("Internal command to resolve worktree path for shell wrapper cd").action((repoName, branch) => {
|
|
32455
33107
|
try {
|
|
32456
33108
|
const config2 = loadConfig();
|
|
@@ -32462,7 +33114,7 @@ program2.command("_resolve-path <repo> <branch>", { hidden: true }).description(
|
|
|
32462
33114
|
}
|
|
32463
33115
|
const repo = repos[0];
|
|
32464
33116
|
const wtPath = getWorktreePath(repo, branch);
|
|
32465
|
-
if (!
|
|
33117
|
+
if (!fs33.existsSync(wtPath)) {
|
|
32466
33118
|
process.stderr.write(`✗ No worktree at ${wtPath}
|
|
32467
33119
|
`);
|
|
32468
33120
|
process.exit(1);
|