@moberg_hr/work-tree 1.5.1 → 1.7.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/dist/wd-bin.js CHANGED
@@ -1573,11 +1573,12 @@ async function upsertSession(target, isGroup, branch, paths, jiraKey, baseBranch
1573
1573
  saveHistory(sessions);
1574
1574
  });
1575
1575
  }
1576
- async function upsertSessionWithPort(target, isGroup, branch, paths, config, jiraKey, baseBranch) {
1576
+ async function upsertSessionWithPort(target, isGroup, branch, paths, config, jiraKey, baseBranch, baseBranches) {
1577
1577
  return withHistoryLock(async () => {
1578
1578
  const sessions = loadHistory();
1579
1579
  const existing = findSession(sessions, target, branch);
1580
1580
  const now = (/* @__PURE__ */ new Date()).toISOString();
1581
+ const hasPerRepo = baseBranches && Object.keys(baseBranches).length > 0;
1581
1582
  let port = existing?.port;
1582
1583
  if (port === void 0) {
1583
1584
  const seedKey = sessionKey(target, branch);
@@ -1592,6 +1593,7 @@ async function upsertSessionWithPort(target, isGroup, branch, paths, config, jir
1592
1593
  existing.lastAccessedAt = now;
1593
1594
  if (jiraKey) existing.jiraKey = jiraKey;
1594
1595
  if (baseBranch && !existing.baseBranch) existing.baseBranch = baseBranch;
1596
+ if (hasPerRepo && !existing.baseBranches) existing.baseBranches = baseBranches;
1595
1597
  if (port !== void 0) existing.port = port;
1596
1598
  } else {
1597
1599
  const session = {
@@ -1604,6 +1606,7 @@ async function upsertSessionWithPort(target, isGroup, branch, paths, config, jir
1604
1606
  };
1605
1607
  if (jiraKey) session.jiraKey = jiraKey;
1606
1608
  if (baseBranch) session.baseBranch = baseBranch;
1609
+ if (hasPerRepo) session.baseBranches = baseBranches;
1607
1610
  if (port !== void 0) session.port = port;
1608
1611
  sessions.push(session);
1609
1612
  }
@@ -1719,6 +1722,62 @@ function copyConfigFiles(repoPath, worktreePath, patterns) {
1719
1722
  }
1720
1723
  }
1721
1724
 
1725
+ // src/core/base-spec.ts
1726
+ var BaseSpecError = class extends Error {
1727
+ constructor(message) {
1728
+ super(message);
1729
+ this.name = "BaseSpecError";
1730
+ }
1731
+ };
1732
+ function parseBaseSpec(raw) {
1733
+ const spec = { perRepo: {} };
1734
+ if (raw === void 0) return spec;
1735
+ const values = Array.isArray(raw) ? raw : [raw];
1736
+ for (const value of values) {
1737
+ const v = value.trim();
1738
+ if (!v) continue;
1739
+ const eq = v.indexOf("=");
1740
+ if (eq === -1) {
1741
+ if (spec.default !== void 0 && spec.default !== v) {
1742
+ throw new BaseSpecError(
1743
+ `Conflicting default --base values: '${spec.default}' and '${v}'`
1744
+ );
1745
+ }
1746
+ spec.default = v;
1747
+ } else {
1748
+ const alias = v.slice(0, eq).trim();
1749
+ const branch = v.slice(eq + 1).trim();
1750
+ if (!alias || !branch) {
1751
+ throw new BaseSpecError(
1752
+ `Invalid --base '${value}'. Use 'alias=branch' or a bare 'branch'.`
1753
+ );
1754
+ }
1755
+ const prior = spec.perRepo[alias];
1756
+ if (prior !== void 0 && prior !== branch) {
1757
+ throw new BaseSpecError(
1758
+ `Conflicting --base for '${alias}': '${prior}' and '${branch}'`
1759
+ );
1760
+ }
1761
+ spec.perRepo[alias] = branch;
1762
+ }
1763
+ }
1764
+ return spec;
1765
+ }
1766
+ function baseForAlias(spec, alias) {
1767
+ return spec.perRepo[alias] ?? spec.default;
1768
+ }
1769
+ function isEmptyBaseSpec(spec) {
1770
+ return spec.default === void 0 && Object.keys(spec.perRepo).length === 0;
1771
+ }
1772
+ function baseSpecOverrideAliases(spec) {
1773
+ return Object.keys(spec.perRepo);
1774
+ }
1775
+ function toBaseSpec(base) {
1776
+ if (base === void 0) return { perRepo: {} };
1777
+ if (typeof base === "string") return { default: base, perRepo: {} };
1778
+ return base;
1779
+ }
1780
+
1722
1781
  // src/core/worktree.ts
1723
1782
  function createSingleWorktree(repoPath, worktreePath, branchName, config, baseBranch) {
1724
1783
  debug("createSingleWorktree", { repoPath, worktreePath, branchName, baseBranch });
@@ -1924,8 +1983,9 @@ function removeSingleWorktree(repoPath, worktreePath, branchName, force) {
1924
1983
  return false;
1925
1984
  }
1926
1985
  }
1927
- async function setupWorktree(targetName, branchName, config, baseBranch, jiraKey) {
1928
- debug("setupWorktree", { targetName, branchName, baseBranch, jiraKey });
1986
+ async function setupWorktree(targetName, branchName, config, base, jiraKey) {
1987
+ const spec = toBaseSpec(base);
1988
+ debug("setupWorktree", { targetName, branchName, spec, jiraKey });
1929
1989
  const target = resolveProjectTarget(targetName, config);
1930
1990
  if (!target) {
1931
1991
  debug("setupWorktree: target not found", targetName);
@@ -1933,27 +1993,38 @@ async function setupWorktree(targetName, branchName, config, baseBranch, jiraKey
1933
1993
  }
1934
1994
  const workTreeDirName = branchName.replace(/\//g, "-");
1935
1995
  if (target.isGroup) {
1936
- return setupGroupWorktree(target.name, target.repoAliases, branchName, workTreeDirName, config, baseBranch, jiraKey);
1996
+ return setupGroupWorktree(target.name, target.repoAliases, branchName, workTreeDirName, config, spec, jiraKey);
1937
1997
  } else {
1938
- return setupSingleWorktree(targetName, branchName, workTreeDirName, config, baseBranch, jiraKey);
1998
+ return setupSingleWorktree(targetName, branchName, workTreeDirName, config, spec, jiraKey);
1939
1999
  }
1940
2000
  }
1941
- async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDirName, config, baseBranch, jiraKey) {
2001
+ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDirName, config, spec, jiraKey) {
1942
2002
  const groupWorktreePath = path12.join(config.worktreesRoot, groupName, workTreeDirName);
1943
- if (baseBranch) {
2003
+ if (!isEmptyBaseSpec(spec)) {
2004
+ const unknownAliases = baseSpecOverrideAliases(spec).filter(
2005
+ (a) => !repoAliases.includes(a)
2006
+ );
2007
+ if (unknownAliases.length > 0) {
2008
+ console.error(
2009
+ `--base names repo(s) not in group '${groupName}': ${unknownAliases.join(", ")}. Group repos: ${repoAliases.join(", ")}`
2010
+ );
2011
+ return null;
2012
+ }
1944
2013
  const missingBase = [];
1945
2014
  const branchExists = [];
1946
2015
  for (const alias of repoAliases) {
1947
2016
  const repoPath = config.repos[alias];
1948
- if (!localBranchExists(baseBranch, repoPath) && !remoteBranchExists(baseBranch, repoPath)) {
1949
- missingBase.push(alias);
2017
+ const repoBase = baseForAlias(spec, alias);
2018
+ if (!repoBase) continue;
2019
+ if (!localBranchExists(repoBase, repoPath) && !remoteBranchExists(repoBase, repoPath)) {
2020
+ missingBase.push(`${alias} (${repoBase})`);
1950
2021
  }
1951
2022
  if (localBranchExists(branchName, repoPath) || remoteBranchExists(branchName, repoPath)) {
1952
2023
  branchExists.push(alias);
1953
2024
  }
1954
2025
  }
1955
2026
  if (missingBase.length > 0) {
1956
- console.error(`Base branch '${baseBranch}' not found in: ${missingBase.join(", ")}`);
2027
+ console.error(`Base branch not found in: ${missingBase.join(", ")}`);
1957
2028
  return null;
1958
2029
  }
1959
2030
  if (branchExists.length > 0) {
@@ -1966,14 +2037,17 @@ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDi
1966
2037
  console.log("");
1967
2038
  fs13.mkdirSync(groupWorktreePath, { recursive: true });
1968
2039
  const createdWorktrees = [];
2040
+ const baseBranches = {};
1969
2041
  for (const alias of repoAliases) {
1970
2042
  const repoPath = config.repos[alias];
1971
2043
  const repoName = path12.basename(repoPath);
1972
2044
  const subWorktreePath = path12.join(groupWorktreePath, repoName);
2045
+ const repoBase = baseForAlias(spec, alias);
1973
2046
  console.log(chalk5.cyan(`[${alias}] (${repoName}):`));
1974
- const success = createSingleWorktree(repoPath, subWorktreePath, branchName, config, baseBranch);
2047
+ const success = createSingleWorktree(repoPath, subWorktreePath, branchName, config, repoBase);
1975
2048
  if (success) {
1976
2049
  createdWorktrees.push({ repoPath, worktreePath: subWorktreePath });
2050
+ if (repoBase) baseBranches[subWorktreePath] = repoBase;
1977
2051
  } else {
1978
2052
  console.log("");
1979
2053
  console.log(chalk5.yellow("Rolling back created worktrees due to failure..."));
@@ -2003,6 +2077,8 @@ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDi
2003
2077
  console.log(chalk5.yellow(`Run 'work config regengroup ${groupName}' to generate it.`));
2004
2078
  }
2005
2079
  const allPaths = createdWorktrees.map((wt) => wt.worktreePath);
2080
+ const distinctBases = [...new Set(Object.values(baseBranches))];
2081
+ const representativeBase = spec.default ?? (distinctBases.length === 1 ? distinctBases[0] : void 0);
2006
2082
  const { port } = await upsertSessionWithPort(
2007
2083
  groupName,
2008
2084
  true,
@@ -2010,17 +2086,26 @@ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDi
2010
2086
  allPaths,
2011
2087
  config,
2012
2088
  jiraKey,
2013
- baseBranch
2089
+ representativeBase,
2090
+ baseBranches
2014
2091
  );
2015
2092
  console.log("");
2016
2093
  console.log(`Branch: ${branchName}`);
2017
2094
  if (port !== void 0) console.log(chalk5.gray(`Dev-server port: ${port}`));
2018
2095
  return { launchDir: groupWorktreePath, paths: allPaths, isGroup: true, port };
2019
2096
  }
2020
- async function setupSingleWorktree(targetName, branchName, workTreeDirName, config, baseBranch, jiraKey) {
2097
+ async function setupSingleWorktree(targetName, branchName, workTreeDirName, config, spec, jiraKey) {
2021
2098
  const repoPath = config.repos[targetName];
2022
2099
  const repoName = path12.basename(repoPath);
2023
2100
  let workTreePath = path12.join(config.worktreesRoot, repoName, workTreeDirName);
2101
+ const unknownAliases = baseSpecOverrideAliases(spec).filter((a) => a !== targetName);
2102
+ if (unknownAliases.length > 0) {
2103
+ console.error(
2104
+ `--base names repo(s) other than '${targetName}': ${unknownAliases.join(", ")}`
2105
+ );
2106
+ return null;
2107
+ }
2108
+ const baseBranch = baseForAlias(spec, targetName);
2024
2109
  if (!fs13.existsSync(repoPath)) {
2025
2110
  console.error(`Repository path does not exist: ${repoPath}`);
2026
2111
  return null;
@@ -2047,7 +2132,8 @@ async function setupSingleWorktree(targetName, branchName, workTreeDirName, conf
2047
2132
  [workTreePath],
2048
2133
  config,
2049
2134
  jiraKey,
2050
- baseBranch
2135
+ baseBranch,
2136
+ baseBranch ? { [workTreePath]: baseBranch } : void 0
2051
2137
  );
2052
2138
  console.log(`Branch: ${branchName}`);
2053
2139
  if (port !== void 0) console.log(chalk5.gray(`Dev-server port: ${port}`));
@@ -2119,7 +2205,7 @@ var treeCommand = {
2119
2205
  type: "boolean",
2120
2206
  default: false
2121
2207
  }).option("base", {
2122
- describe: "Create the new branch from this base branch instead of HEAD",
2208
+ describe: "Base branch to fork from instead of HEAD. Repeatable. Use a bare branch (--base dev) for all repos, or alias=branch (--base backend=dev --base frontend=feat/x) for per-repo bases in a group.",
2123
2209
  type: "string"
2124
2210
  }).option("prompt", {
2125
2211
  describe: "Initial prompt to send to the AI tool on startup",
@@ -2144,7 +2230,17 @@ var treeCommand = {
2144
2230
  const open = argv.open;
2145
2231
  const unsafe = argv.unsafe;
2146
2232
  const setupOnly = argv["setup-only"];
2147
- const baseBranch = argv.base;
2233
+ let baseSpec;
2234
+ try {
2235
+ baseSpec = parseBaseSpec(argv.base);
2236
+ } catch (err) {
2237
+ if (err instanceof BaseSpecError) {
2238
+ console.error(err.message);
2239
+ process.exitCode = 1;
2240
+ return;
2241
+ }
2242
+ throw err;
2243
+ }
2148
2244
  const jiraKey = argv["jira-key"];
2149
2245
  const promptFile = argv["prompt-file"];
2150
2246
  let initialPrompt = argv.prompt;
@@ -2182,12 +2278,10 @@ var treeCommand = {
2182
2278
  process.exitCode = 1;
2183
2279
  return;
2184
2280
  }
2185
- if (baseBranch && !branchName) {
2281
+ if (!isEmptyBaseSpec(baseSpec) && !branchName) {
2186
2282
  console.error("--base requires a branch name");
2187
2283
  console.log(
2188
- chalk6.yellow(
2189
- `Usage: work tree ${targetName} <branch> --base ${baseBranch}`
2190
- )
2284
+ chalk6.yellow(`Usage: work tree ${targetName} <branch> --base <base>`)
2191
2285
  );
2192
2286
  process.exitCode = 1;
2193
2287
  return;
@@ -2235,7 +2329,7 @@ var treeCommand = {
2235
2329
  }
2236
2330
  return;
2237
2331
  }
2238
- const result = await setupWorktree(targetName, branchName, config, baseBranch, jiraKey);
2332
+ const result = await setupWorktree(targetName, branchName, config, baseSpec, jiraKey);
2239
2333
  if (!result) {
2240
2334
  process.exitCode = 1;
2241
2335
  return;
@@ -6335,9 +6429,9 @@ var hydrateCommand = {
6335
6429
  };
6336
6430
 
6337
6431
  // src/commands/diff.ts
6338
- import fs35 from "fs";
6432
+ import fs36 from "fs";
6339
6433
  import os9 from "os";
6340
- import path32 from "path";
6434
+ import path33 from "path";
6341
6435
  import { spawn as childSpawn } from "child_process";
6342
6436
  import chalk21 from "chalk";
6343
6437
 
@@ -7039,11 +7133,12 @@ function resolveBase(scope, argv) {
7039
7133
  }
7040
7134
  return { base: "HEAD", source: "default" };
7041
7135
  }
7042
- function buildRepoSpecs(scope, base) {
7136
+ function buildRepoSpecs(scope, base, perRepoBase) {
7043
7137
  return scope.repos.map((r) => {
7044
- let diffArg = base;
7045
- if (base !== "HEAD") {
7046
- const mb = git(["merge-base", base, "HEAD"], r.root);
7138
+ const repoBase = perRepoBase?.[r.root] ?? base;
7139
+ let diffArg = repoBase;
7140
+ if (repoBase !== "HEAD") {
7141
+ const mb = git(["merge-base", repoBase, "HEAD"], r.root);
7047
7142
  if (mb.exitCode === 0 && mb.stdout) diffArg = mb.stdout;
7048
7143
  }
7049
7144
  return { name: r.name, root: r.root, diffArg };
@@ -7060,6 +7155,17 @@ function resolveRepoDiff(root, base, sessionBaseBranch) {
7060
7155
  if (mb.exitCode === 0 && mb.stdout) diffArg = mb.stdout;
7061
7156
  return { resolvedBase: parent, diffArg };
7062
7157
  }
7158
+ function sessionBaseForPath(root) {
7159
+ const norm = normPath(root);
7160
+ for (const s of loadHistory()) {
7161
+ for (const p of s.paths) {
7162
+ if (normPath(p) === norm) {
7163
+ return s.baseBranches?.[p] ?? s.baseBranch;
7164
+ }
7165
+ }
7166
+ }
7167
+ return void 0;
7168
+ }
7063
7169
 
7064
7170
  // src/core/comment-server.ts
7065
7171
  import { Hono as Hono2 } from "hono";
@@ -7147,9 +7253,56 @@ import { Hono } from "hono";
7147
7253
  import { serve } from "@hono/node-server";
7148
7254
  import { streamSSE } from "hono/streaming";
7149
7255
 
7150
- // src/core/fs-watcher.ts
7256
+ // src/core/file-context.ts
7151
7257
  import fs31 from "fs";
7152
7258
  import path28 from "path";
7259
+ import spawn10 from "cross-spawn";
7260
+ function readContextLines(opts) {
7261
+ const { root, relPath, ref } = opts;
7262
+ if (!relPath || !isInsideRoot(root, relPath)) return null;
7263
+ const start = Math.max(1, Math.floor(opts.start));
7264
+ const end = Math.max(start, Math.floor(opts.end));
7265
+ const content = !ref || ref === "working" ? readWorkingTree(root, relPath) : readAtRef(root, ref, relPath);
7266
+ if (content === null) return null;
7267
+ const all = content.split(/\r?\n/);
7268
+ if (all.length > 0 && all[all.length - 1] === "") all.pop();
7269
+ const totalLines = all.length;
7270
+ const slice = all.slice(start - 1, end);
7271
+ return {
7272
+ lines: slice,
7273
+ start,
7274
+ totalLines,
7275
+ eof: end >= totalLines
7276
+ };
7277
+ }
7278
+ function readWorkingTree(root, relPath) {
7279
+ try {
7280
+ const absPath = path28.join(root, relPath);
7281
+ const realRoot2 = fs31.realpathSync(path28.resolve(root));
7282
+ const realPath = fs31.realpathSync(absPath);
7283
+ const sep = path28.sep;
7284
+ if (realPath !== realRoot2 && !realPath.startsWith(realRoot2 + sep)) {
7285
+ return null;
7286
+ }
7287
+ return fs31.readFileSync(absPath, "utf-8");
7288
+ } catch {
7289
+ return null;
7290
+ }
7291
+ }
7292
+ function readAtRef(root, ref, relPath) {
7293
+ const r = spawn10.sync("git", ["show", `${ref}:${relPath}`], {
7294
+ cwd: root,
7295
+ encoding: "utf-8",
7296
+ maxBuffer: 64 * 1024 * 1024,
7297
+ windowsHide: true
7298
+ });
7299
+ if (r.status === 0 && typeof r.stdout === "string") return r.stdout;
7300
+ return null;
7301
+ }
7302
+
7303
+ // src/core/fs-watcher.ts
7304
+ import fs32 from "fs";
7305
+ import path29 from "path";
7153
7306
  import chalk19 from "chalk";
7154
7307
  import chokidar from "chokidar";
7155
7308
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
@@ -7175,7 +7328,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
7175
7328
  ]);
7176
7329
  function isIgnoredWatchPath(roots, filePath) {
7177
7330
  for (const root of roots) {
7178
- const rel = path28.relative(root, filePath).replace(/\\/g, "/");
7331
+ const rel = path29.relative(root, filePath).replace(/\\/g, "/");
7179
7332
  if (rel === "" || rel.startsWith("../")) continue;
7180
7333
  if (rel.split("/").some((seg) => IGNORED_DIRS.has(seg))) return true;
7181
7334
  }
@@ -7199,8 +7352,8 @@ function createFsWatcher(opts) {
7199
7352
  };
7200
7353
  if (SUPPORTS_RECURSIVE_WATCH) {
7201
7354
  const watchers = opts.roots.map((root) => {
7202
- const w = fs31.watch(root, { recursive: true }, (_event, filename) => {
7203
- if (filename && isIgnoredWatchPath([root], path28.join(root, filename.toString()))) {
7355
+ const w = fs32.watch(root, { recursive: true }, (_event, filename) => {
7356
+ if (filename && isIgnoredWatchPath([root], path29.join(root, filename.toString()))) {
7204
7357
  return;
7205
7358
  }
7206
7359
  fire();
@@ -7238,30 +7391,30 @@ function createFsWatcher(opts) {
7238
7391
  }
7239
7392
 
7240
7393
  // src/core/web-static.ts
7241
- import fs32 from "fs";
7242
- import path29 from "path";
7394
+ import fs33 from "fs";
7395
+ import path30 from "path";
7243
7396
  import { fileURLToPath } from "url";
7244
7397
  function resolveWebRoot() {
7245
- const entryDir = path29.dirname(process.argv[1] ?? "");
7246
- const moduleDir = path29.dirname(fileURLToPath(import.meta.url));
7398
+ const entryDir = path30.dirname(process.argv[1] ?? "");
7399
+ const moduleDir = path30.dirname(fileURLToPath(import.meta.url));
7247
7400
  const candidates = [
7248
- path29.join(entryDir, "web"),
7401
+ path30.join(entryDir, "web"),
7249
7402
  // bundled: this module is inlined into dist/<bin>.js, so dist/web is a
7250
7403
  // sibling of the bundle. Works even when argv[1] is an npm bin symlink
7251
7404
  // (which is not realpath'd, so the entryDir candidate above misses).
7252
- path29.join(moduleDir, "web"),
7405
+ path30.join(moduleDir, "web"),
7253
7406
  // dev/tsx fallback: walk up from src/core to repo root then into dist/web.
7254
- path29.resolve(moduleDir, "../../dist/web")
7407
+ path30.resolve(moduleDir, "../../dist/web")
7255
7408
  ];
7256
7409
  for (const c of candidates) {
7257
- if (fs32.existsSync(path29.join(c, "index.html"))) return c;
7410
+ if (fs33.existsSync(path30.join(c, "index.html"))) return c;
7258
7411
  }
7259
7412
  return null;
7260
7413
  }
7261
7414
 
7262
7415
  // src/core/spa-handler.ts
7263
- import fs33 from "fs";
7264
- import path30 from "path";
7416
+ import fs34 from "fs";
7417
+ import path31 from "path";
7265
7418
  var MIME = {
7266
7419
  ".html": "text/html; charset=utf-8",
7267
7420
  ".js": "application/javascript; charset=utf-8",
@@ -7275,18 +7428,18 @@ var MIME = {
7275
7428
  function readFile(root, relPath) {
7276
7429
  const clean = relPath.split("?")[0];
7277
7430
  const requested = clean === "/" ? "/index.html" : clean;
7278
- const filePath = path30.join(root, requested);
7279
- const norm = path30.normalize(filePath);
7280
- if (!norm.startsWith(path30.normalize(root))) return null;
7431
+ const filePath = path31.join(root, requested);
7432
+ const norm = path31.normalize(filePath);
7433
+ if (!norm.startsWith(path31.normalize(root))) return null;
7281
7434
  let stat;
7282
7435
  try {
7283
- stat = fs33.statSync(norm);
7436
+ stat = fs34.statSync(norm);
7284
7437
  } catch {
7285
7438
  return null;
7286
7439
  }
7287
7440
  if (!stat.isFile()) return null;
7288
- const ext = path30.extname(norm).toLowerCase();
7289
- return { body: fs33.readFileSync(norm), ext };
7441
+ const ext = path31.extname(norm).toLowerCase();
7442
+ return { body: fs34.readFileSync(norm), ext };
7290
7443
  }
7291
7444
  function serveSpa(c, webRoot) {
7292
7445
  const url = new URL(c.req.url);
@@ -7342,7 +7495,11 @@ async function startDiffServer(opts) {
7342
7495
  const base = c.req.query("base") === "branch" ? "branch" : "uncommitted";
7343
7496
  try {
7344
7497
  const resolved = opts.repos.map(
7345
- (r) => base === "uncommitted" ? { resolvedBase: "HEAD", diffArg: r.diffArg } : resolveRepoDiff(r.root, "branch", opts.sessionBaseBranch)
7498
+ (r) => base === "uncommitted" ? { resolvedBase: "HEAD", diffArg: r.diffArg } : resolveRepoDiff(
7499
+ r.root,
7500
+ "branch",
7501
+ opts.sessionBaseBranches?.[r.root] ?? opts.sessionBaseBranch
7502
+ )
7346
7503
  );
7347
7504
  const repos = opts.repos.map((r, i) => ({
7348
7505
  name: r.name,
@@ -7356,6 +7513,21 @@ async function startDiffServer(opts) {
7356
7513
  return c.json({ error: err.message }, 500);
7357
7514
  }
7358
7515
  });
7516
+ app.get("/api/file-lines", (c) => {
7517
+ const repoName = c.req.query("repo") ?? "";
7518
+ const relPath = c.req.query("path") ?? "";
7519
+ const start = Number(c.req.query("start"));
7520
+ const end = Number(c.req.query("end"));
7521
+ const ref = c.req.query("ref") || void 0;
7522
+ if (!relPath || !Number.isInteger(start) || !Number.isInteger(end)) {
7523
+ return c.json({ error: "bad path/start/end" }, 400);
7524
+ }
7525
+ const root = opts.repos.length === 1 ? opts.repos[0].root : opts.repos.find((r) => r.name === repoName)?.root;
7526
+ if (!root) return c.json({ error: "unknown repo" }, 404);
7527
+ const result = readContextLines({ root, relPath, start, end, ref });
7528
+ if (!result) return c.json({ error: "cannot read file" }, 400);
7529
+ return c.json(result);
7530
+ });
7359
7531
  app.get(
7360
7532
  "/events",
7361
7533
  (c) => streamSSE(c, async (stream) => {
@@ -7518,6 +7690,7 @@ async function startCommentServer(opts) {
7518
7690
  repos: opts.repos,
7519
7691
  scopeLabel: opts.scopeLabel,
7520
7692
  sessionBaseBranch: opts.sessionBaseBranch,
7693
+ sessionBaseBranches: opts.sessionBaseBranches,
7521
7694
  watchDebounceMs: opts.watchDebounceMs,
7522
7695
  attachRoutes
7523
7696
  });
@@ -7559,8 +7732,8 @@ function diffReviewSnapshot(snapshot, seen) {
7559
7732
  }
7560
7733
 
7561
7734
  // src/core/static-renderer.ts
7562
- import fs34 from "fs";
7563
- import path31 from "path";
7735
+ import fs35 from "fs";
7736
+ import path32 from "path";
7564
7737
  function buildDiff(specs, resolvedBase) {
7565
7738
  return {
7566
7739
  repos: specs.map((r) => ({
@@ -7576,8 +7749,8 @@ function renderStatic(opts) {
7576
7749
  if (!webRoot) {
7577
7750
  throw new Error("Could not find dist/web/. Run `npm run build` first.");
7578
7751
  }
7579
- const shellPath = path31.join(webRoot, "index.html");
7580
- let shell = fs34.readFileSync(shellPath, "utf-8");
7752
+ const shellPath = path32.join(webRoot, "index.html");
7753
+ let shell = fs35.readFileSync(shellPath, "utf-8");
7581
7754
  const uncommitted = buildDiff(opts.uncommitted, "HEAD");
7582
7755
  const branch = opts.branch ? buildDiff(opts.branch.specs, opts.branch.resolvedBase) : void 0;
7583
7756
  const initialBase = opts.initialBase ?? "uncommitted";
@@ -7626,10 +7799,10 @@ function escapeForScriptTag(json) {
7626
7799
  }
7627
7800
  function readAsset(webRoot, urlPath) {
7628
7801
  const clean = urlPath.split("?")[0].replace(/^\//, "");
7629
- const full = path31.join(webRoot, clean);
7630
- if (!path31.normalize(full).startsWith(path31.normalize(webRoot))) return null;
7802
+ const full = path32.join(webRoot, clean);
7803
+ if (!path32.normalize(full).startsWith(path32.normalize(webRoot))) return null;
7631
7804
  try {
7632
- return fs34.readFileSync(full, "utf-8");
7805
+ return fs35.readFileSync(full, "utf-8");
7633
7806
  } catch {
7634
7807
  return null;
7635
7808
  }
@@ -7670,22 +7843,22 @@ async function runStop(repoSpecs) {
7670
7843
  }
7671
7844
  }
7672
7845
  function webUrlFilePath() {
7673
- return path32.join(os9.homedir(), ".work", "web.url");
7846
+ return path33.join(os9.homedir(), ".work", "web.url");
7674
7847
  }
7675
7848
  function resolveWorkBinPath(selfArgv1) {
7676
7849
  let real = selfArgv1;
7677
7850
  try {
7678
- real = fs35.realpathSync(selfArgv1);
7851
+ real = fs36.realpathSync(selfArgv1);
7679
7852
  } catch {
7680
7853
  }
7681
7854
  if (real.endsWith("wd-bin.js")) {
7682
- return path32.join(path32.dirname(real), "bin.js");
7855
+ return path33.join(path33.dirname(real), "bin.js");
7683
7856
  }
7684
7857
  return real;
7685
7858
  }
7686
7859
  function readWebUrl() {
7687
7860
  try {
7688
- const v = fs35.readFileSync(webUrlFilePath(), "utf-8").trim();
7861
+ const v = fs36.readFileSync(webUrlFilePath(), "utf-8").trim();
7689
7862
  return v || null;
7690
7863
  } catch {
7691
7864
  return null;
@@ -7695,8 +7868,8 @@ async function ensureWorkWebRunning() {
7695
7868
  const existing = readWebUrl();
7696
7869
  if (existing) return existing;
7697
7870
  const workBin = resolveWorkBinPath(process.argv[1]);
7698
- const out = fs35.openSync(
7699
- path32.join(os9.homedir(), ".work", "web-autostart.log"),
7871
+ const out = fs36.openSync(
7872
+ path33.join(os9.homedir(), ".work", "web-autostart.log"),
7700
7873
  "a"
7701
7874
  );
7702
7875
  const child = childSpawn(
@@ -7711,7 +7884,7 @@ async function ensureWorkWebRunning() {
7711
7884
  }
7712
7885
  );
7713
7886
  child.unref();
7714
- fs35.closeSync(out);
7887
+ fs36.closeSync(out);
7715
7888
  const url = await waitForUrlFile(webUrlFilePath(), 5e3);
7716
7889
  return url;
7717
7890
  }
@@ -7755,9 +7928,10 @@ async function runLauncher(ctx) {
7755
7928
  function runStatic(ctx, initialBranch) {
7756
7929
  const uncommitted = buildRepoSpecs(ctx.scope, "HEAD");
7757
7930
  const primaryRoot = ctx.scope.repos.find((r) => r.name === ctx.scope.activeRepoName)?.root ?? ctx.scope.repos[0].root;
7758
- const parent = ctx.scope.session?.baseBranch ?? findAnyParentBranch(primaryRoot);
7931
+ const perRepoBase = ctx.scope.session?.baseBranches;
7932
+ const parent = perRepoBase?.[primaryRoot] ?? ctx.scope.session?.baseBranch ?? findAnyParentBranch(primaryRoot);
7759
7933
  const branch = parent === null ? void 0 : {
7760
- specs: buildRepoSpecs(ctx.scope, parent),
7934
+ specs: buildRepoSpecs(ctx.scope, parent, perRepoBase),
7761
7935
  resolvedBase: parent
7762
7936
  };
7763
7937
  const uncommittedTotal = uncommitted.reduce(
@@ -7779,7 +7953,7 @@ function runStatic(ctx, initialBranch) {
7779
7953
  initialBase: initialBranch && branch ? "branch" : "uncommitted"
7780
7954
  });
7781
7955
  const filePath = `${ctx.scopeStem}.html`;
7782
- fs35.writeFileSync(filePath, html, "utf-8");
7956
+ fs36.writeFileSync(filePath, html, "utf-8");
7783
7957
  info(chalk21.gray(`Wrote ${filePath}`));
7784
7958
  openUrl(`file:///${filePath.replace(/\\/g, "/")}`);
7785
7959
  }
@@ -7788,7 +7962,7 @@ function waitForUrlFile(filePath, timeoutMs) {
7788
7962
  const start = Date.now();
7789
7963
  const tick = () => {
7790
7964
  try {
7791
- const v = fs35.readFileSync(filePath, "utf-8").trim();
7965
+ const v = fs36.readFileSync(filePath, "utf-8").trim();
7792
7966
  if (v) return resolve(v);
7793
7967
  } catch {
7794
7968
  }
@@ -7928,6 +8102,7 @@ summary-id: ${info3.summary.id}` : ""}
7928
8102
  repos: ctx.repoSpecs,
7929
8103
  scopeLabel: ctx.scopeLabel,
7930
8104
  sessionBaseBranch: ctx.scope.session?.baseBranch,
8105
+ sessionBaseBranches: ctx.scope.session?.baseBranches,
7931
8106
  onComment,
7932
8107
  onCommentDeleted,
7933
8108
  onSubmitReviewStart,
@@ -8034,21 +8209,21 @@ var diffCommand = {
8034
8209
  };
8035
8210
 
8036
8211
  // src/commands/web.ts
8037
- import fs40 from "fs";
8212
+ import fs41 from "fs";
8038
8213
  import os13 from "os";
8039
- import path41 from "path";
8214
+ import path42 from "path";
8040
8215
  import chalk23 from "chalk";
8041
8216
 
8042
8217
  // src/core/web-server.ts
8043
- import fs39 from "fs";
8218
+ import fs40 from "fs";
8044
8219
  import os12 from "os";
8045
- import path40 from "path";
8220
+ import path41 from "path";
8046
8221
  import chalk22 from "chalk";
8047
8222
  import { Hono as Hono3 } from "hono";
8048
8223
  import { streamSSE as streamSSE3 } from "hono/streaming";
8049
8224
 
8050
8225
  // src/core/web-state.ts
8051
- import path33 from "path";
8226
+ import path34 from "path";
8052
8227
  import crypto4 from "crypto";
8053
8228
  import chokidar2 from "chokidar";
8054
8229
  function sessionIdFor(s) {
@@ -8069,7 +8244,7 @@ function subscribeSession(sessionId, onChange) {
8069
8244
  const watcher = chokidar2.watch(roots, {
8070
8245
  ignored: (filePath) => {
8071
8246
  for (const r of roots) {
8072
- const rel = path33.relative(r, filePath).replace(/\\/g, "/");
8247
+ const rel = path34.relative(r, filePath).replace(/\\/g, "/");
8073
8248
  if (rel === ".git" || rel.startsWith(".git/")) return true;
8074
8249
  }
8075
8250
  return false;
@@ -8183,24 +8358,24 @@ function disposeAllPtys() {
8183
8358
  }
8184
8359
 
8185
8360
  // src/core/comment-file-store.ts
8186
- import fs36 from "fs";
8187
- import path34 from "path";
8361
+ import fs37 from "fs";
8362
+ import path35 from "path";
8188
8363
  import os10 from "os";
8189
8364
  function commentsDir() {
8190
- return path34.join(os10.homedir(), ".work", "comments");
8365
+ return path35.join(os10.homedir(), ".work", "comments");
8191
8366
  }
8192
8367
  function ensureDir() {
8193
- fs36.mkdirSync(commentsDir(), { recursive: true });
8368
+ fs37.mkdirSync(commentsDir(), { recursive: true });
8194
8369
  }
8195
8370
  function commentsFileFor(sessionId) {
8196
- return path34.join(commentsDir(), `${sessionId}.json`);
8371
+ return path35.join(commentsDir(), `${sessionId}.json`);
8197
8372
  }
8198
8373
  function pathFor(sessionId) {
8199
8374
  return commentsFileFor(sessionId);
8200
8375
  }
8201
8376
  function readDisk(sessionId) {
8202
8377
  try {
8203
- const raw = fs36.readFileSync(pathFor(sessionId), "utf-8");
8378
+ const raw = fs37.readFileSync(pathFor(sessionId), "utf-8");
8204
8379
  const parsed = JSON.parse(raw);
8205
8380
  return Array.isArray(parsed) ? parsed : [];
8206
8381
  } catch {
@@ -8272,29 +8447,29 @@ function clearCommentStoreCache() {
8272
8447
  }
8273
8448
 
8274
8449
  // src/core/pending-delivery.ts
8275
- import fs37 from "fs";
8276
- import path35 from "path";
8450
+ import fs38 from "fs";
8451
+ import path36 from "path";
8277
8452
  function pathFor2(sessionId) {
8278
8453
  return {
8279
8454
  comments: commentsFileFor(sessionId),
8280
- delivered: path35.join(commentsDir(), `${sessionId}.delivered.json`)
8455
+ delivered: path36.join(commentsDir(), `${sessionId}.delivered.json`)
8281
8456
  };
8282
8457
  }
8283
8458
  function readJson(filePath, fallback) {
8284
8459
  try {
8285
- return JSON.parse(fs37.readFileSync(filePath, "utf8"));
8460
+ return JSON.parse(fs38.readFileSync(filePath, "utf8"));
8286
8461
  } catch {
8287
8462
  return fallback;
8288
8463
  }
8289
8464
  }
8290
8465
  function writeAtomic2(filePath, content) {
8291
- fs37.mkdirSync(path35.dirname(filePath), { recursive: true });
8466
+ fs38.mkdirSync(path36.dirname(filePath), { recursive: true });
8292
8467
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
8293
- fs37.writeFileSync(tmp, content, "utf-8");
8294
- fs37.renameSync(tmp, filePath);
8468
+ fs38.writeFileSync(tmp, content, "utf-8");
8469
+ fs38.renameSync(tmp, filePath);
8295
8470
  }
8296
8471
  function normalize(p) {
8297
- return path35.resolve(p).replace(/\\/g, "/").toLowerCase();
8472
+ return path36.resolve(p).replace(/\\/g, "/").toLowerCase();
8298
8473
  }
8299
8474
  function findSessionForCwd(cwd) {
8300
8475
  const norm = normalize(cwd);
@@ -8325,7 +8500,7 @@ function readPendingForSession(sessionId) {
8325
8500
  return comments.filter(isPendingFor(delivered));
8326
8501
  }
8327
8502
  function scopeStoreIdsForPaths(paths) {
8328
- const resolved = paths.map((p) => path35.resolve(p));
8503
+ const resolved = paths.map((p) => path36.resolve(p));
8329
8504
  const ids = /* @__PURE__ */ new Set();
8330
8505
  ids.add(`scope-${scopeHashFor(resolved)}`);
8331
8506
  for (const p of resolved) ids.add(`scope-${scopeHashFor([p])}`);
@@ -8648,8 +8823,8 @@ function mountPanesRoutes(app, opts) {
8648
8823
  }
8649
8824
 
8650
8825
  // src/core/worktree-routes.ts
8651
- import path36 from "path";
8652
- import { spawn as spawn10 } from "child_process";
8826
+ import path37 from "path";
8827
+ import { spawn as spawn11 } from "child_process";
8653
8828
  import { zValidator as zValidator4 } from "@hono/zod-validator";
8654
8829
  import { z as z3 } from "zod";
8655
8830
  function mountWorktreeRoutes(app, opts) {
@@ -8772,10 +8947,10 @@ function mountWorktreeRoutes(app, opts) {
8772
8947
  const id = c.req.param("id");
8773
8948
  const session = findSession2(id);
8774
8949
  if (!session) return c.json({ error: "unknown session" }, 404);
8775
- const target = session.isGroup ? path36.dirname(session.paths[0]) : session.paths[0];
8950
+ const target = session.isGroup ? path37.dirname(session.paths[0]) : session.paths[0];
8776
8951
  try {
8777
8952
  const cmd = process.platform === "win32" ? "code.cmd" : "code";
8778
- const child = spawn10(cmd, [target], {
8953
+ const child = spawn11(cmd, [target], {
8779
8954
  detached: true,
8780
8955
  stdio: "ignore",
8781
8956
  shell: false
@@ -8792,27 +8967,27 @@ function mountWorktreeRoutes(app, opts) {
8792
8967
  import { zValidator as zValidator5 } from "@hono/zod-validator";
8793
8968
  import { z as z4 } from "zod";
8794
8969
  import { EventEmitter } from "events";
8795
- import path39 from "path";
8796
- import spawn12 from "cross-spawn";
8970
+ import path40 from "path";
8971
+ import spawn13 from "cross-spawn";
8797
8972
 
8798
8973
  // src/core/checkpoint.ts
8799
- import fs38 from "fs";
8974
+ import fs39 from "fs";
8800
8975
  import os11 from "os";
8801
- import path37 from "path";
8802
- import spawn11 from "cross-spawn";
8976
+ import path38 from "path";
8977
+ import spawn12 from "cross-spawn";
8803
8978
  function manifestPath(scopeHash) {
8804
- const dir = path37.join(os11.homedir(), ".work", "diffs");
8805
- fs38.mkdirSync(dir, { recursive: true });
8806
- return path37.join(dir, `${scopeHash}.checkpoints.json`);
8979
+ const dir = path38.join(os11.homedir(), ".work", "diffs");
8980
+ fs39.mkdirSync(dir, { recursive: true });
8981
+ return path38.join(dir, `${scopeHash}.checkpoints.json`);
8807
8982
  }
8808
8983
  function emptyManifest(scopeHash) {
8809
8984
  return { version: 1, scopeHash, entries: [] };
8810
8985
  }
8811
8986
  function loadManifest(scopeHash) {
8812
8987
  const file = manifestPath(scopeHash);
8813
- if (!fs38.existsSync(file)) return emptyManifest(scopeHash);
8988
+ if (!fs39.existsSync(file)) return emptyManifest(scopeHash);
8814
8989
  try {
8815
- const raw = fs38.readFileSync(file, "utf-8");
8990
+ const raw = fs39.readFileSync(file, "utf-8");
8816
8991
  const parsed = JSON.parse(raw);
8817
8992
  if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
8818
8993
  return emptyManifest(scopeHash);
@@ -8841,7 +9016,7 @@ function snapshotRepo(repoRoot, scopeHash, id, includeWorkingTree = true) {
8841
9016
  GIT_COMMITTER_EMAIL: "wd@local",
8842
9017
  GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
8843
9018
  };
8844
- const commit = spawn11.sync("git", commitArgs, {
9019
+ const commit = spawn12.sync("git", commitArgs, {
8845
9020
  cwd: repoRoot,
8846
9021
  encoding: "utf-8",
8847
9022
  env: commitEnv,
@@ -8850,7 +9025,7 @@ function snapshotRepo(repoRoot, scopeHash, id, includeWorkingTree = true) {
8850
9025
  if (commit.status !== 0 || !commit.stdout) return null;
8851
9026
  const commitSha = commit.stdout.trim();
8852
9027
  const refName = `refs/wd/${scopeHash}/${id}`;
8853
- const updateRef = spawn11.sync("git", ["update-ref", refName, commitSha], {
9028
+ const updateRef = spawn12.sync("git", ["update-ref", refName, commitSha], {
8854
9029
  cwd: repoRoot,
8855
9030
  encoding: "utf-8",
8856
9031
  windowsHide: true
@@ -8877,7 +9052,7 @@ async function takeCheckpoint(scopeHash, repos, opts = {}) {
8877
9052
  const rollbackRefs = () => {
8878
9053
  const refName = `refs/wd/${scopeHash}/${nextId}`;
8879
9054
  for (const repo of repos) {
8880
- spawn11.sync("git", ["update-ref", "-d", refName], {
9055
+ spawn12.sync("git", ["update-ref", "-d", refName], {
8881
9056
  cwd: repo.root,
8882
9057
  encoding: "utf-8",
8883
9058
  windowsHide: true
@@ -8892,7 +9067,7 @@ async function takeCheckpoint(scopeHash, repos, opts = {}) {
8892
9067
  const prev = manifest.entries[manifest.entries.length - 1];
8893
9068
  const treeOf = (root, commitSha) => {
8894
9069
  if (!commitSha) return null;
8895
- const r = spawn11.sync(
9070
+ const r = spawn12.sync(
8896
9071
  "git",
8897
9072
  ["rev-parse", `${commitSha}^{tree}`],
8898
9073
  { cwd: root, encoding: "utf-8", windowsHide: true }
@@ -8927,7 +9102,7 @@ function clearCheckpoints(scopeHash, repoRoots) {
8927
9102
  for (const root of repoRoots) {
8928
9103
  for (const entry of manifest.entries) {
8929
9104
  const refName = `refs/wd/${scopeHash}/${entry.id}`;
8930
- spawn11.sync("git", ["update-ref", "-d", refName], {
9105
+ spawn12.sync("git", ["update-ref", "-d", refName], {
8931
9106
  cwd: root,
8932
9107
  encoding: "utf-8",
8933
9108
  windowsHide: true
@@ -8935,16 +9110,16 @@ function clearCheckpoints(scopeHash, repoRoots) {
8935
9110
  }
8936
9111
  }
8937
9112
  const file = manifestPath(scopeHash);
8938
- if (fs38.existsSync(file)) {
9113
+ if (fs39.existsSync(file)) {
8939
9114
  try {
8940
- fs38.unlinkSync(file);
9115
+ fs39.unlinkSync(file);
8941
9116
  } catch {
8942
9117
  }
8943
9118
  }
8944
9119
  }
8945
9120
 
8946
9121
  // src/core/scope-manager.ts
8947
- import path38 from "path";
9122
+ import path39 from "path";
8948
9123
  import crypto5 from "crypto";
8949
9124
  var scopes = /* @__PURE__ */ new Map();
8950
9125
  function hashFor(paths) {
@@ -8961,7 +9136,7 @@ var ScopePathRejectedError = class extends Error {
8961
9136
  }
8962
9137
  };
8963
9138
  function normaliseForCompare(p) {
8964
- return path38.resolve(p).replace(/\\/g, "/").toLowerCase();
9139
+ return path39.resolve(p).replace(/\\/g, "/").toLowerCase();
8965
9140
  }
8966
9141
  function rejectedPaths(normalised) {
8967
9142
  const config = loadConfig();
@@ -8979,7 +9154,7 @@ function rejectedPaths(normalised) {
8979
9154
  });
8980
9155
  }
8981
9156
  function registerScope(paths, label2) {
8982
- const normalised = paths.map((p) => path38.resolve(p));
9157
+ const normalised = paths.map((p) => path39.resolve(p));
8983
9158
  const rejected = rejectedPaths(normalised);
8984
9159
  if (rejected.length > 0) throw new ScopePathRejectedError(rejected);
8985
9160
  const hash = hashFor(normalised);
@@ -8993,7 +9168,7 @@ function registerScope(paths, label2) {
8993
9168
  const scope = {
8994
9169
  hash,
8995
9170
  paths: normalised,
8996
- label: label2 ?? path38.basename(normalised[0]),
9171
+ label: label2 ?? path39.basename(normalised[0]),
8997
9172
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8998
9173
  ended: false
8999
9174
  };
@@ -9066,7 +9241,7 @@ function mountScopeRoutes(app, opts) {
9066
9241
  function workingTreeFingerprint(paths) {
9067
9242
  const parts = [];
9068
9243
  for (const p of paths) {
9069
- const r = spawn12.sync(
9244
+ const r = spawn13.sync(
9070
9245
  "git",
9071
9246
  ["status", "--porcelain", "--no-renames", "-z"],
9072
9247
  { cwd: p, encoding: "utf-8", windowsHide: true }
@@ -9190,7 +9365,7 @@ function mountScopeRoutes(app, opts) {
9190
9365
  const fromSha = fromEntry.repos[p] ?? "HEAD";
9191
9366
  const toSha = toEntry === void 0 ? "working" : toEntry.repos[p] ?? "HEAD";
9192
9367
  return {
9193
- name: path39.basename(p),
9368
+ name: path40.basename(p),
9194
9369
  root: p,
9195
9370
  files: computeRangeDiff({ root: p, fromRef: fromSha, toRef: toSha })
9196
9371
  };
@@ -9204,9 +9379,11 @@ function mountScopeRoutes(app, opts) {
9204
9379
  repos: repos2
9205
9380
  });
9206
9381
  }
9207
- const resolved = scope.paths.map((p) => resolveRepoDiff(p, base));
9382
+ const resolved = scope.paths.map(
9383
+ (p) => resolveRepoDiff(p, base, sessionBaseForPath(p))
9384
+ );
9208
9385
  const repos = scope.paths.map((p, i) => ({
9209
- name: path39.basename(p),
9386
+ name: path40.basename(p),
9210
9387
  root: p,
9211
9388
  resolvedBase: resolved[i].resolvedBase,
9212
9389
  files: computeDiff({ root: p, diffArg: resolved[i].diffArg })
@@ -9225,6 +9402,23 @@ function mountScopeRoutes(app, opts) {
9225
9402
  return c.json({ error: err.message }, 500);
9226
9403
  }
9227
9404
  });
9405
+ app.get("/api/scopes/:hash/file-lines", (c) => {
9406
+ const scope = getScope(c.req.param("hash"));
9407
+ if (!scope) return c.json({ error: "unknown scope" }, 404);
9408
+ const repoName = c.req.query("repo") ?? "";
9409
+ const relPath = c.req.query("path") ?? "";
9410
+ const start = Number(c.req.query("start"));
9411
+ const end = Number(c.req.query("end"));
9412
+ const ref = c.req.query("ref") || void 0;
9413
+ if (!relPath || !Number.isInteger(start) || !Number.isInteger(end)) {
9414
+ return c.json({ error: "bad path/start/end" }, 400);
9415
+ }
9416
+ const root = scope.paths.length === 1 ? scope.paths[0] : scope.paths.find((p) => path40.basename(p) === repoName);
9417
+ if (!root) return c.json({ error: "unknown repo" }, 404);
9418
+ const result = readContextLines({ root, relPath, start, end, ref });
9419
+ if (!result) return c.json({ error: "cannot read file" }, 400);
9420
+ return c.json(result);
9421
+ });
9228
9422
  app.get("/api/scopes/:hash/checkpoints", (c) => {
9229
9423
  const scope = getScope(c.req.param("hash"));
9230
9424
  if (!scope) return c.json({ error: "unknown scope" }, 404);
@@ -9453,9 +9647,11 @@ function sessionToWire(s) {
9453
9647
  };
9454
9648
  }
9455
9649
  function computeSessionDiff(s, base) {
9456
- const resolved = s.paths.map((p) => resolveRepoDiff(p, base, s.baseBranch));
9650
+ const resolved = s.paths.map(
9651
+ (p) => resolveRepoDiff(p, base, s.baseBranches?.[p] ?? s.baseBranch)
9652
+ );
9457
9653
  const repos = s.paths.map((p, i) => ({
9458
- name: path40.basename(p),
9654
+ name: path41.basename(p),
9459
9655
  root: p,
9460
9656
  resolvedBase: resolved[i].resolvedBase,
9461
9657
  files: computeDiff({ root: p, diffArg: resolved[i].diffArg })
@@ -9475,17 +9671,17 @@ async function startWebServer(opts = {}) {
9475
9671
  for (const cb of sseListeners) cb({ event, data });
9476
9672
  };
9477
9673
  const home = os12.homedir();
9478
- const historyPath = path40.join(home, ".work", "history.json");
9674
+ const historyPath = path41.join(home, ".work", "history.json");
9479
9675
  const onHistoryChange = () => broadcast("sessions-changed", { ts: Date.now() });
9480
- fs39.watchFile(historyPath, { interval: 1e3 }, onHistoryChange);
9481
- const tasksPath = path40.join(home, ".work", "tasks.json");
9676
+ fs40.watchFile(historyPath, { interval: 1e3 }, onHistoryChange);
9677
+ const tasksPath = path41.join(home, ".work", "tasks.json");
9482
9678
  const onTasksChange = () => broadcast("tasks-changed", { ts: Date.now() });
9483
- fs39.watchFile(tasksPath, { interval: 1e3 }, onTasksChange);
9679
+ fs40.watchFile(tasksPath, { interval: 1e3 }, onTasksChange);
9484
9680
  const projectsRoot = claudeProjectsRoot();
9485
9681
  let activityWatcher = null;
9486
9682
  if (!lean) {
9487
9683
  try {
9488
- if (fs39.existsSync(projectsRoot)) {
9684
+ if (fs40.existsSync(projectsRoot)) {
9489
9685
  activityWatcher = createFsWatcher({
9490
9686
  roots: [projectsRoot],
9491
9687
  debounceMs: 250,
@@ -9560,8 +9756,8 @@ async function startWebServer(opts = {}) {
9560
9756
  url: handle.url,
9561
9757
  port: handle.port,
9562
9758
  stop: async () => {
9563
- fs39.unwatchFile(historyPath, onHistoryChange);
9564
- fs39.unwatchFile(tasksPath, onTasksChange);
9759
+ fs40.unwatchFile(historyPath, onHistoryChange);
9760
+ fs40.unwatchFile(tasksPath, onTasksChange);
9565
9761
  if (decayTick) clearInterval(decayTick);
9566
9762
  activityWatcher?.stop();
9567
9763
  disposeAllWatchers();
@@ -9623,10 +9819,10 @@ function info2(message) {
9623
9819
  process.stderr.write(message + "\n");
9624
9820
  }
9625
9821
  function urlFilePath() {
9626
- return path41.join(os13.homedir(), ".work", "web.url");
9822
+ return path42.join(os13.homedir(), ".work", "web.url");
9627
9823
  }
9628
9824
  function pidFilePath() {
9629
- return path41.join(os13.homedir(), ".work", "web.pid");
9825
+ return path42.join(os13.homedir(), ".work", "web.pid");
9630
9826
  }
9631
9827
  function isPidAlive(pid) {
9632
9828
  try {
@@ -9638,7 +9834,7 @@ function isPidAlive(pid) {
9638
9834
  }
9639
9835
  function readPid() {
9640
9836
  try {
9641
- const raw = fs40.readFileSync(pidFilePath(), "utf-8").trim();
9837
+ const raw = fs41.readFileSync(pidFilePath(), "utf-8").trim();
9642
9838
  const n = Number(raw);
9643
9839
  return Number.isFinite(n) && n > 0 ? n : null;
9644
9840
  } catch {
@@ -9647,7 +9843,7 @@ function readPid() {
9647
9843
  }
9648
9844
  function readUrl() {
9649
9845
  try {
9650
- const v = fs40.readFileSync(urlFilePath(), "utf-8").trim();
9846
+ const v = fs41.readFileSync(urlFilePath(), "utf-8").trim();
9651
9847
  return v || null;
9652
9848
  } catch {
9653
9849
  return null;
@@ -9673,11 +9869,11 @@ function stopExisting() {
9673
9869
  if (!isPidAlive(pid)) {
9674
9870
  info2(chalk23.gray(`Stale PID ${pid} \u2014 cleaning up.`));
9675
9871
  try {
9676
- fs40.unlinkSync(pidFilePath());
9872
+ fs41.unlinkSync(pidFilePath());
9677
9873
  } catch {
9678
9874
  }
9679
9875
  try {
9680
- fs40.unlinkSync(urlFilePath());
9876
+ fs41.unlinkSync(urlFilePath());
9681
9877
  } catch {
9682
9878
  }
9683
9879
  return false;
@@ -9686,11 +9882,11 @@ function stopExisting() {
9686
9882
  process.kill(pid);
9687
9883
  info2(chalk23.gray(`Stopped work web (PID ${pid}).`));
9688
9884
  try {
9689
- fs40.unlinkSync(pidFilePath());
9885
+ fs41.unlinkSync(pidFilePath());
9690
9886
  } catch {
9691
9887
  }
9692
9888
  try {
9693
- fs40.unlinkSync(urlFilePath());
9889
+ fs41.unlinkSync(urlFilePath());
9694
9890
  } catch {
9695
9891
  }
9696
9892
  return true;
@@ -9741,19 +9937,19 @@ var webCommand = {
9741
9937
  process.exit(1);
9742
9938
  }
9743
9939
  try {
9744
- fs40.unlinkSync(pidFilePath());
9940
+ fs41.unlinkSync(pidFilePath());
9745
9941
  } catch {
9746
9942
  }
9747
9943
  try {
9748
- fs40.unlinkSync(urlFilePath());
9944
+ fs41.unlinkSync(urlFilePath());
9749
9945
  } catch {
9750
9946
  }
9751
9947
  const lean = !!argv.lean || process.env.WORK_WEB_LEAN === "1";
9752
9948
  const handle = await startWebServer({ lean });
9753
9949
  try {
9754
- fs40.mkdirSync(path41.dirname(urlFilePath()), { recursive: true });
9755
- fs40.writeFileSync(urlFilePath(), handle.url);
9756
- fs40.writeFileSync(pidFilePath(), String(process.pid));
9950
+ fs41.mkdirSync(path42.dirname(urlFilePath()), { recursive: true });
9951
+ fs41.writeFileSync(urlFilePath(), handle.url);
9952
+ fs41.writeFileSync(pidFilePath(), String(process.pid));
9757
9953
  } catch {
9758
9954
  }
9759
9955
  info2(
@@ -9783,11 +9979,11 @@ var webCommand = {
9783
9979
  const shutdown = () => {
9784
9980
  info2(chalk23.gray("\nStopping work web."));
9785
9981
  try {
9786
- fs40.unlinkSync(urlFilePath());
9982
+ fs41.unlinkSync(urlFilePath());
9787
9983
  } catch {
9788
9984
  }
9789
9985
  try {
9790
- fs40.unlinkSync(pidFilePath());
9986
+ fs41.unlinkSync(pidFilePath());
9791
9987
  } catch {
9792
9988
  }
9793
9989
  if (!lean) {
@@ -9807,11 +10003,11 @@ var webCommand = {
9807
10003
  process.on("SIGTERM", shutdown);
9808
10004
  process.on("exit", () => {
9809
10005
  try {
9810
- fs40.unlinkSync(pidFilePath());
10006
+ fs41.unlinkSync(pidFilePath());
9811
10007
  } catch {
9812
10008
  }
9813
10009
  try {
9814
- fs40.unlinkSync(urlFilePath());
10010
+ fs41.unlinkSync(urlFilePath());
9815
10011
  } catch {
9816
10012
  }
9817
10013
  });
@@ -9886,7 +10082,7 @@ var hookCommand = {
9886
10082
 
9887
10083
  // src/commands/run.ts
9888
10084
  import chalk24 from "chalk";
9889
- import spawn13 from "cross-spawn";
10085
+ import spawn14 from "cross-spawn";
9890
10086
 
9891
10087
  // src/core/fleet.ts
9892
10088
  function selectSessions(sessions, filter) {
@@ -9928,7 +10124,7 @@ function killAllChildren(signal = "SIGTERM") {
9928
10124
  function runInPath(unit, cmd, prefix) {
9929
10125
  const { bin, args } = shellInvocation(cmd);
9930
10126
  return new Promise((resolve) => {
9931
- const child = spawn13(bin, args, {
10127
+ const child = spawn14(bin, args, {
9932
10128
  cwd: unit.path,
9933
10129
  stdio: prefix ? ["ignore", "pipe", "pipe"] : "inherit",
9934
10130
  shell: false
@@ -10160,15 +10356,15 @@ var runCommand = {
10160
10356
  };
10161
10357
 
10162
10358
  // src/commands/broadcast.ts
10163
- import fs42 from "fs";
10359
+ import fs43 from "fs";
10164
10360
  import chalk25 from "chalk";
10165
10361
 
10166
10362
  // src/core/broadcast.ts
10167
10363
  import crypto6 from "crypto";
10168
- import fs41 from "fs";
10364
+ import fs42 from "fs";
10169
10365
  function readComments(file) {
10170
10366
  try {
10171
- const parsed = JSON.parse(fs41.readFileSync(file, "utf-8"));
10367
+ const parsed = JSON.parse(fs42.readFileSync(file, "utf-8"));
10172
10368
  return Array.isArray(parsed) ? parsed : [];
10173
10369
  } catch {
10174
10370
  return [];
@@ -10176,7 +10372,7 @@ function readComments(file) {
10176
10372
  }
10177
10373
  async function appendLocked(sessionId, body) {
10178
10374
  const file = commentsFileFor(sessionId);
10179
- fs41.mkdirSync(commentsDir(), { recursive: true });
10375
+ fs42.mkdirSync(commentsDir(), { recursive: true });
10180
10376
  ensureFile(file, "[]");
10181
10377
  const comment = {
10182
10378
  id: crypto6.randomBytes(8).toString("hex"),
@@ -10213,7 +10409,7 @@ async function broadcastPrompt(sessions, filter, prompt) {
10213
10409
  // src/commands/broadcast.ts
10214
10410
  function readStdin() {
10215
10411
  try {
10216
- return fs42.readFileSync(0, "utf-8");
10412
+ return fs43.readFileSync(0, "utf-8");
10217
10413
  } catch {
10218
10414
  return "";
10219
10415
  }
@@ -10288,8 +10484,8 @@ var broadcastCommand = {
10288
10484
  };
10289
10485
 
10290
10486
  // src/completions/index.ts
10291
- import fs43 from "fs";
10292
- import path42 from "path";
10487
+ import fs44 from "fs";
10488
+ import path43 from "path";
10293
10489
  function completionHandler(current, argv, done) {
10294
10490
  const rawArgs = argv._;
10295
10491
  const args = rawArgs.slice(1, -1);
@@ -10393,7 +10589,7 @@ function completeTreeRemoveList(command, args, current, config, done) {
10393
10589
  }
10394
10590
  function completeRepoBranches(alias, current, config, done) {
10395
10591
  const repoPath = config.repos[alias];
10396
- if (!repoPath || !fs43.existsSync(repoPath)) {
10592
+ if (!repoPath || !fs44.existsSync(repoPath)) {
10397
10593
  done([]);
10398
10594
  return;
10399
10595
  }
@@ -10402,19 +10598,19 @@ function completeRepoBranches(alias, current, config, done) {
10402
10598
  done(branches);
10403
10599
  }
10404
10600
  function completeGroupBranches(groupName, repoAliases, current, config, done) {
10405
- const groupDir = path42.join(config.worktreesRoot, groupName);
10406
- if (!fs43.existsSync(groupDir)) {
10601
+ const groupDir = path43.join(config.worktreesRoot, groupName);
10602
+ if (!fs44.existsSync(groupDir)) {
10407
10603
  done([]);
10408
10604
  return;
10409
10605
  }
10410
10606
  try {
10411
- const branchDirs = fs43.readdirSync(groupDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
10412
- const bdPath = path42.join(groupDir, d.name);
10607
+ const branchDirs = fs44.readdirSync(groupDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
10608
+ const bdPath = path43.join(groupDir, d.name);
10413
10609
  for (const alias of repoAliases) {
10414
10610
  const repoPath = config.repos[alias];
10415
10611
  if (!repoPath) continue;
10416
- const subPath = path42.join(bdPath, path42.basename(repoPath));
10417
- if (fs43.existsSync(subPath)) {
10612
+ const subPath = path43.join(bdPath, path43.basename(repoPath));
10613
+ if (fs44.existsSync(subPath)) {
10418
10614
  const branch = getCurrentBranch(subPath);
10419
10615
  if (branch) return branch;
10420
10616
  }
@@ -10428,7 +10624,7 @@ function completeGroupBranches(groupName, repoAliases, current, config, done) {
10428
10624
  }
10429
10625
 
10430
10626
  // src/version.ts
10431
- var VERSION = true ? "1.5.1" : "dev";
10627
+ var VERSION = true ? "1.7.0" : "dev";
10432
10628
 
10433
10629
  // src/cli.ts
10434
10630
  function showHelp() {