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