@moberg_hr/work-tree 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/wd-bin.js CHANGED
@@ -307,10 +307,15 @@ function fetchRemote(cwd) {
307
307
  }
308
308
  async function fetchRemoteAsync(cwd) {
309
309
  await new Promise((resolve, reject) => {
310
- execFile("git", ["fetch", "--quiet"], { cwd, timeout: 3e4 }, (err) => {
311
- if (err) reject(err);
312
- else resolve();
313
- });
310
+ execFile(
311
+ "git",
312
+ ["fetch", "--quiet"],
313
+ { cwd, timeout: 3e4, windowsHide: true },
314
+ (err) => {
315
+ if (err) reject(err);
316
+ else resolve();
317
+ }
318
+ );
314
319
  });
315
320
  if (!getDefaultBranch(cwd)) {
316
321
  git(["remote", "set-head", "origin", "--auto"], cwd);
@@ -409,7 +414,8 @@ function generateGroupClaudeMd(groupName, repoAliases, config) {
409
414
  const result = spawn2.sync("claude", ["-p"], {
410
415
  input: prompt,
411
416
  encoding: "utf-8",
412
- stdio: ["pipe", "pipe", "pipe"]
417
+ stdio: ["pipe", "pipe", "pipe"],
418
+ windowsHide: true
413
419
  });
414
420
  let content;
415
421
  if (result.status !== 0 || !result.stdout?.trim()) {
@@ -871,7 +877,11 @@ function getWindowsDocumentsFolder() {
871
877
  const result = spawn4.sync(
872
878
  exe,
873
879
  ["-NoProfile", "-Command", "[Environment]::GetFolderPath('MyDocuments')"],
874
- { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
880
+ {
881
+ encoding: "utf-8",
882
+ stdio: ["pipe", "pipe", "pipe"],
883
+ windowsHide: true
884
+ }
875
885
  );
876
886
  if (result.status === 0 && result.stdout) {
877
887
  const dir = result.stdout.toString().trim();
@@ -884,7 +894,8 @@ function executableExists(name) {
884
894
  const cmd = process.platform === "win32" ? "where" : "which";
885
895
  const result = spawn4.sync(cmd, [name], {
886
896
  encoding: "utf-8",
887
- stdio: ["pipe", "pipe", "pipe"]
897
+ stdio: ["pipe", "pipe", "pipe"],
898
+ windowsHide: true
888
899
  });
889
900
  return result.status === 0;
890
901
  }
@@ -2793,10 +2804,8 @@ function collectPrunable(config, options = {}) {
2793
2804
  for (const [alias, repoPath] of Object.entries(config.repos)) {
2794
2805
  if (!fs19.existsSync(repoPath)) continue;
2795
2806
  if (skipAliases.has(alias)) continue;
2796
- const worktrees = parseWorktreeList(repoPath);
2797
- const normalizedRepoPath = path16.resolve(repoPath);
2807
+ const worktrees = parseWorktreeList(repoPath).slice(1);
2798
2808
  for (const wt of worktrees) {
2799
- if (path16.resolve(wt.path) === normalizedRepoPath) continue;
2800
2809
  if (!wt.branch) continue;
2801
2810
  if (groupCoveredKeys.has(`${alias}:${wt.branch}`)) continue;
2802
2811
  const { merged, into, confidence } = isBranchMerged(wt.branch, repoPath);
@@ -3126,10 +3135,15 @@ import spawn7 from "cross-spawn";
3126
3135
  import { execFile as execFile2 } from "child_process";
3127
3136
  function execAsync(cmd, args, cwd, timeout) {
3128
3137
  return new Promise((resolve, reject) => {
3129
- execFile2(cmd, args, { cwd, encoding: "utf-8", timeout }, (err, stdout) => {
3130
- if (err) reject(err);
3131
- else resolve(stdout ?? "");
3132
- });
3138
+ execFile2(
3139
+ cmd,
3140
+ args,
3141
+ { cwd, encoding: "utf-8", timeout, windowsHide: true },
3142
+ (err, stdout) => {
3143
+ if (err) reject(err);
3144
+ else resolve(stdout ?? "");
3145
+ }
3146
+ );
3133
3147
  });
3134
3148
  }
3135
3149
  function parsePrJson(stdout, repoAlias, currentUser) {
@@ -3245,10 +3259,15 @@ async function isGhAvailable() {
3245
3259
  import { execFile as execFile3 } from "child_process";
3246
3260
  function execAsync2(cmd, args, timeout) {
3247
3261
  return new Promise((resolve, reject) => {
3248
- execFile3(cmd, args, { encoding: "utf-8", timeout }, (err, stdout) => {
3249
- if (err) reject(err);
3250
- else resolve(stdout ?? "");
3251
- });
3262
+ execFile3(
3263
+ cmd,
3264
+ args,
3265
+ { encoding: "utf-8", timeout, windowsHide: true },
3266
+ (err, stdout) => {
3267
+ if (err) reject(err);
3268
+ else resolve(stdout ?? "");
3269
+ }
3270
+ );
3252
3271
  });
3253
3272
  }
3254
3273
  async function isAcliAvailable() {
@@ -4663,7 +4682,7 @@ function generateSlug(summary) {
4663
4682
  const child = execFile4(
4664
4683
  "claude",
4665
4684
  ["-p", "--model", "haiku"],
4666
- { encoding: "utf-8", timeout: 1e4 },
4685
+ { encoding: "utf-8", timeout: 1e4, windowsHide: true },
4667
4686
  (err, stdout) => {
4668
4687
  const result = stdout?.trim().toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/^-|-$/g, "");
4669
4688
  resolve(result || fallback);
@@ -6316,18 +6335,16 @@ var hydrateCommand = {
6316
6335
  };
6317
6336
 
6318
6337
  // src/commands/diff.ts
6319
- import fs32 from "fs";
6338
+ import fs35 from "fs";
6320
6339
  import os9 from "os";
6321
- import path31 from "path";
6340
+ import path32 from "path";
6322
6341
  import { spawn as childSpawn } from "child_process";
6323
6342
  import chalk21 from "chalk";
6324
6343
 
6325
6344
  // src/core/diff-pipeline.ts
6326
- import fs27 from "fs";
6327
- import os7 from "os";
6328
- import path24 from "path";
6329
- import crypto from "crypto";
6330
- import spawn8 from "cross-spawn";
6345
+ import fs28 from "fs";
6346
+ import path25 from "path";
6347
+ import spawn9 from "cross-spawn";
6331
6348
 
6332
6349
  // src/core/diff-parse.ts
6333
6350
  var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
@@ -6619,19 +6636,65 @@ function coverageLookup(root, relPaths) {
6619
6636
  return { byPath: out, lcovMtimeMs: read.mtimeMs };
6620
6637
  }
6621
6638
 
6639
+ // src/core/git-tree-snapshot.ts
6640
+ import fs27 from "fs";
6641
+ import os7 from "os";
6642
+ import path24 from "path";
6643
+ import crypto from "crypto";
6644
+ import spawn8 from "cross-spawn";
6645
+ function writeTempTree(repoRoot, opts = {}) {
6646
+ const includeWorkingTree = opts.includeWorkingTree ?? true;
6647
+ const tmpIndex = path24.join(
6648
+ os7.tmpdir(),
6649
+ `wd-tree-${process.pid}-${crypto.randomBytes(6).toString("hex")}.idx`
6650
+ );
6651
+ const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
6652
+ const run2 = (args) => spawn8.sync("git", args, {
6653
+ cwd: repoRoot,
6654
+ encoding: "utf-8",
6655
+ env,
6656
+ windowsHide: true,
6657
+ maxBuffer: 64 * 1024 * 1024
6658
+ });
6659
+ try {
6660
+ const headSha = (spawn8.sync("git", ["rev-parse", "--verify", "HEAD"], {
6661
+ cwd: repoRoot,
6662
+ encoding: "utf-8",
6663
+ windowsHide: true
6664
+ }).stdout ?? "").trim() || null;
6665
+ if (headSha) {
6666
+ const r = run2(["read-tree", "HEAD"]);
6667
+ if (r.status !== 0) return null;
6668
+ }
6669
+ if (includeWorkingTree) {
6670
+ const add = run2(["add", "-A"]);
6671
+ if (add.status !== 0) return null;
6672
+ }
6673
+ const wt = run2(["write-tree"]);
6674
+ if (wt.status !== 0 || !wt.stdout) return null;
6675
+ return { treeSha: wt.stdout.trim(), headSha };
6676
+ } finally {
6677
+ try {
6678
+ if (fs27.existsSync(tmpIndex)) fs27.unlinkSync(tmpIndex);
6679
+ } catch {
6680
+ }
6681
+ }
6682
+ }
6683
+
6622
6684
  // src/core/diff-pipeline.ts
6685
+ var RENAME_DETECT = "-M";
6623
6686
  var MARKDOWN_EXT_RE = /\.(md|markdown|mdx)$/i;
6624
6687
  var MARKDOWN_SIZE_CAP = 256 * 1024;
6625
6688
  function isMarkdownPath(p) {
6626
6689
  return p !== "/dev/null" && MARKDOWN_EXT_RE.test(p);
6627
6690
  }
6628
6691
  function isInsideRoot(root, rel) {
6629
- const resolvedRoot = path24.resolve(root);
6630
- const resolvedTarget = path24.resolve(resolvedRoot, rel);
6631
- const r = path24.relative(resolvedRoot, resolvedTarget);
6692
+ const resolvedRoot = path25.resolve(root);
6693
+ const resolvedTarget = path25.resolve(resolvedRoot, rel);
6694
+ const r = path25.relative(resolvedRoot, resolvedTarget);
6632
6695
  if (r === "") return true;
6633
6696
  if (r.startsWith("..")) return false;
6634
- if (path24.isAbsolute(r)) return false;
6697
+ if (path25.isAbsolute(r)) return false;
6635
6698
  return true;
6636
6699
  }
6637
6700
  function readMarkdownContent(root, file, fromRef, toRef) {
@@ -6640,7 +6703,7 @@ function readMarkdownContent(root, file, fromRef, toRef) {
6640
6703
  return void 0;
6641
6704
  }
6642
6705
  const showAt = (ref, p) => {
6643
- const r = spawn8.sync("git", ["show", `${ref}:${p}`], {
6706
+ const r = spawn9.sync("git", ["show", `${ref}:${p}`], {
6644
6707
  cwd: root,
6645
6708
  encoding: "utf-8",
6646
6709
  maxBuffer: 16 * 1024 * 1024,
@@ -6656,12 +6719,12 @@ function readMarkdownContent(root, file, fromRef, toRef) {
6656
6719
  if (file.status !== "deleted" && isMarkdownPath(file.newPath) && isInsideRoot(root, file.newPath)) {
6657
6720
  if (toRef === "working") {
6658
6721
  try {
6659
- const absPath = path24.join(root, file.newPath);
6660
- const realRoot2 = fs27.realpathSync(path24.resolve(root));
6661
- const realPath = fs27.realpathSync(absPath);
6662
- const sep = path24.sep;
6722
+ const absPath = path25.join(root, file.newPath);
6723
+ const realRoot2 = fs28.realpathSync(path25.resolve(root));
6724
+ const realPath = fs28.realpathSync(absPath);
6725
+ const sep = path25.sep;
6663
6726
  if (realPath === realRoot2 || realPath.startsWith(realRoot2 + sep)) {
6664
- result.after = fs27.readFileSync(absPath, "utf-8");
6727
+ result.after = fs28.readFileSync(absPath, "utf-8");
6665
6728
  }
6666
6729
  } catch {
6667
6730
  }
@@ -6679,39 +6742,7 @@ function readMarkdownContent(root, file, fromRef, toRef) {
6679
6742
  return result;
6680
6743
  }
6681
6744
  function workingTreeTreeSha(root) {
6682
- const tmpIndex = path24.join(
6683
- os7.tmpdir(),
6684
- `wd-diff-${process.pid}-${crypto.randomBytes(6).toString("hex")}.idx`
6685
- );
6686
- const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
6687
- const run2 = (args) => spawn8.sync("git", args, {
6688
- cwd: root,
6689
- encoding: "utf-8",
6690
- env,
6691
- windowsHide: true,
6692
- maxBuffer: 64 * 1024 * 1024
6693
- });
6694
- try {
6695
- const headExists = (spawn8.sync("git", ["rev-parse", "--verify", "HEAD"], {
6696
- cwd: root,
6697
- encoding: "utf-8",
6698
- windowsHide: true
6699
- }).stdout ?? "").trim().length > 0;
6700
- if (headExists) {
6701
- const r = run2(["read-tree", "HEAD"]);
6702
- if (r.status !== 0) return null;
6703
- }
6704
- const add = run2(["add", "-A"]);
6705
- if (add.status !== 0) return null;
6706
- const wt = run2(["write-tree"]);
6707
- if (wt.status !== 0 || !wt.stdout) return null;
6708
- return wt.stdout.trim();
6709
- } finally {
6710
- try {
6711
- if (fs27.existsSync(tmpIndex)) fs27.unlinkSync(tmpIndex);
6712
- } catch {
6713
- }
6714
- }
6745
+ return writeTempTree(root, { includeWorkingTree: true })?.treeSha ?? null;
6715
6746
  }
6716
6747
  function isBinaryContent(buffer) {
6717
6748
  const len = Math.min(buffer.length, 8192);
@@ -6721,10 +6752,10 @@ function isBinaryContent(buffer) {
6721
6752
  return false;
6722
6753
  }
6723
6754
  function synthesizeUntrackedDiff(root, relPath) {
6724
- const absPath = path24.join(root, relPath);
6755
+ const absPath = path25.join(root, relPath);
6725
6756
  let buffer;
6726
6757
  try {
6727
- buffer = fs27.readFileSync(absPath);
6758
+ buffer = fs28.readFileSync(absPath);
6728
6759
  } catch {
6729
6760
  return "";
6730
6761
  }
@@ -6760,11 +6791,13 @@ new file mode 100644
6760
6791
  }
6761
6792
  function computeDiff(opts) {
6762
6793
  const { root, diffArg } = opts;
6763
- const trackedResult = spawn8.sync(
6794
+ const trackedResult = spawn9.sync(
6764
6795
  "git",
6765
6796
  // -w (ignore-all-space) hides pure whitespace changes so a reformat
6766
- // of indentation doesn't drown out the real changes.
6767
- ["diff", "--no-color", "--no-ext-diff", "-w", diffArg],
6797
+ // of indentation doesn't drown out the real changes. RENAME_DETECT turns
6798
+ // on rename detection so a `git mv` (+ edits) shows as one renamed entry
6799
+ // with its inline diff instead of a separate delete + add pair.
6800
+ ["diff", "--no-color", "--no-ext-diff", "-w", RENAME_DETECT, diffArg],
6768
6801
  {
6769
6802
  cwd: root,
6770
6803
  encoding: "utf-8",
@@ -6810,7 +6843,7 @@ function attachCoverage(root, files) {
6810
6843
  f.coverageMtimeMs = lcovMtimeMs;
6811
6844
  let srcMtimeMs = null;
6812
6845
  try {
6813
- srcMtimeMs = fs27.statSync(path24.join(root, f.path)).mtimeMs;
6846
+ srcMtimeMs = fs28.statSync(path25.join(root, f.path)).mtimeMs;
6814
6847
  } catch {
6815
6848
  srcMtimeMs = null;
6816
6849
  }
@@ -6828,9 +6861,12 @@ function computeRangeDiff(opts) {
6828
6861
  }
6829
6862
  const wtTreeSha = workingTreeTreeSha(root);
6830
6863
  if (!wtTreeSha) return [];
6831
- const result2 = spawn8.sync(
6864
+ const result2 = spawn9.sync(
6832
6865
  "git",
6833
- ["diff-tree", "-r", "-p", "--no-color", "--no-ext-diff", fromRef, wtTreeSha],
6866
+ // RENAME_DETECT: detect renames between the checkpoint tree and the
6867
+ // working tree so a rename (with or without edits) renders as one
6868
+ // entry, matching the HEAD-vs-working path above.
6869
+ ["diff-tree", "-r", "-p", RENAME_DETECT, "--no-color", "--no-ext-diff", fromRef, wtTreeSha],
6834
6870
  {
6835
6871
  cwd: root,
6836
6872
  encoding: "utf-8",
@@ -6849,9 +6885,11 @@ function computeRangeDiff(opts) {
6849
6885
  }
6850
6886
  return parsed;
6851
6887
  }
6852
- const result = spawn8.sync(
6888
+ const result = spawn9.sync(
6853
6889
  "git",
6854
- ["diff", "--no-color", "--no-ext-diff", fromRef, toRef],
6890
+ // RENAME_DETECT: rename detection between two checkpoint commits (no -w
6891
+ // here — see the note above on why range diffs keep whitespace changes).
6892
+ ["diff", "--no-color", "--no-ext-diff", RENAME_DETECT, fromRef, toRef],
6855
6893
  {
6856
6894
  cwd: root,
6857
6895
  encoding: "utf-8",
@@ -6872,69 +6910,82 @@ function computeRangeDiff(opts) {
6872
6910
  }
6873
6911
 
6874
6912
  // src/core/repo-spec.ts
6875
- import fs28 from "fs";
6876
- import path25 from "path";
6913
+ import fs29 from "fs";
6914
+ import path26 from "path";
6877
6915
  import os8 from "os";
6878
6916
  import crypto2 from "crypto";
6879
- function stableDiffPath(keyPaths) {
6917
+ function scopeHashFor(keyPaths) {
6880
6918
  const key = keyPaths.slice().sort().join("|");
6881
- const id = crypto2.createHash("sha1").update(key).digest("hex").slice(0, 12);
6882
- const dir = path25.join(os8.homedir(), ".work", "diffs");
6883
- fs28.mkdirSync(dir, { recursive: true });
6884
- return path25.join(dir, id);
6919
+ return crypto2.createHash("sha1").update(key).digest("hex").slice(0, 12);
6920
+ }
6921
+ function stableDiffPath(keyPaths) {
6922
+ const dir = path26.join(os8.homedir(), ".work", "diffs");
6923
+ fs29.mkdirSync(dir, { recursive: true });
6924
+ return path26.join(dir, scopeHashFor(keyPaths));
6885
6925
  }
6886
6926
 
6887
6927
  // src/core/diff-scope.ts
6888
- import path26 from "path";
6928
+ import fs30 from "fs";
6929
+ import path27 from "path";
6889
6930
  import chalk18 from "chalk";
6890
6931
  function normPath(p) {
6891
- return path26.resolve(p).replace(/\\/g, "/").toLowerCase();
6932
+ return path27.resolve(p).replace(/\\/g, "/").toLowerCase();
6933
+ }
6934
+ function realNorm(p) {
6935
+ try {
6936
+ return normPath(fs30.realpathSync(p));
6937
+ } catch {
6938
+ return normPath(p);
6939
+ }
6892
6940
  }
6893
6941
  function resolveScope(cwd) {
6894
6942
  const normCwd = normPath(cwd);
6895
6943
  const sessions = loadHistory();
6944
+ const top = git(["rev-parse", "--show-toplevel"], cwd);
6945
+ const toplevel = top.exitCode === 0 && top.stdout ? top.stdout : null;
6946
+ const realTop = toplevel ? realNorm(toplevel) : null;
6896
6947
  for (const s of sessions) {
6897
6948
  for (const p of s.paths) {
6898
6949
  const np = normPath(p);
6899
6950
  if (normCwd === np || normCwd.startsWith(np + "/")) {
6951
+ if (realTop && realNorm(p) !== realTop) continue;
6900
6952
  if (s.isGroup) {
6901
6953
  return {
6902
6954
  isGroup: true,
6903
6955
  session: s,
6904
- repos: s.paths.map((rp) => ({ name: path26.basename(rp), root: rp })),
6905
- activeRepoName: path26.basename(p)
6956
+ repos: s.paths.map((rp) => ({ name: path27.basename(rp), root: rp })),
6957
+ activeRepoName: path27.basename(p)
6906
6958
  };
6907
6959
  }
6908
6960
  return {
6909
6961
  isGroup: false,
6910
6962
  session: s,
6911
- repos: [{ name: path26.basename(p), root: p }],
6912
- activeRepoName: path26.basename(p)
6963
+ repos: [{ name: path27.basename(p), root: p }],
6964
+ activeRepoName: path27.basename(p)
6913
6965
  };
6914
6966
  }
6915
6967
  }
6916
6968
  }
6917
6969
  for (const s of sessions) {
6918
6970
  if (!s.isGroup || s.paths.length === 0) continue;
6919
- const parents = s.paths.map((p) => normPath(path26.dirname(p)));
6971
+ const parents = s.paths.map((p) => normPath(path27.dirname(p)));
6920
6972
  const groupRoot = parents[0];
6921
6973
  if (!parents.every((par) => par === groupRoot)) continue;
6922
6974
  if (normCwd === groupRoot || normCwd.startsWith(groupRoot + "/")) {
6923
6975
  return {
6924
6976
  isGroup: true,
6925
6977
  session: s,
6926
- repos: s.paths.map((rp) => ({ name: path26.basename(rp), root: rp })),
6978
+ repos: s.paths.map((rp) => ({ name: path27.basename(rp), root: rp })),
6927
6979
  activeRepoName: null
6928
6980
  };
6929
6981
  }
6930
6982
  }
6931
- const toplevel = git(["rev-parse", "--show-toplevel"], cwd);
6932
- if (toplevel.exitCode !== 0 || !toplevel.stdout) return null;
6983
+ if (!toplevel) return null;
6933
6984
  return {
6934
6985
  isGroup: false,
6935
6986
  session: null,
6936
- repos: [{ name: path26.basename(toplevel.stdout), root: toplevel.stdout }],
6937
- activeRepoName: path26.basename(toplevel.stdout)
6987
+ repos: [{ name: path27.basename(toplevel), root: toplevel }],
6988
+ activeRepoName: path27.basename(toplevel)
6938
6989
  };
6939
6990
  }
6940
6991
  function findAnyParentBranch(cwd) {
@@ -7002,7 +7053,7 @@ function resolveRepoDiff(root, base, sessionBaseBranch) {
7002
7053
  if (base === "uncommitted") {
7003
7054
  return { resolvedBase: "HEAD", diffArg: "HEAD" };
7004
7055
  }
7005
- const parent = sessionBaseBranch ?? findAnyParentBranch(root);
7056
+ const parent = sessionBaseBranch ?? detectParentBranch(root) ?? findAnyParentBranch(root);
7006
7057
  if (!parent) return { resolvedBase: "HEAD", diffArg: "HEAD" };
7007
7058
  let diffArg = "HEAD";
7008
7059
  const mb = git(["merge-base", parent, "HEAD"], root);
@@ -7097,36 +7148,86 @@ import { serve } from "@hono/node-server";
7097
7148
  import { streamSSE } from "hono/streaming";
7098
7149
 
7099
7150
  // src/core/fs-watcher.ts
7100
- import path27 from "path";
7151
+ import fs31 from "fs";
7152
+ import path28 from "path";
7101
7153
  import chalk19 from "chalk";
7102
7154
  import chokidar from "chokidar";
7155
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
7156
+ ".git",
7157
+ "node_modules",
7158
+ "bin",
7159
+ // .NET build output
7160
+ "obj",
7161
+ // .NET build output
7162
+ "dist",
7163
+ "build",
7164
+ "out",
7165
+ "target",
7166
+ // Rust / JVM
7167
+ ".next",
7168
+ ".nuxt",
7169
+ ".svelte-kit",
7170
+ ".turbo",
7171
+ ".gradle",
7172
+ "coverage",
7173
+ ".vs",
7174
+ ".idea"
7175
+ ]);
7176
+ function isIgnoredWatchPath(roots, filePath) {
7177
+ for (const root of roots) {
7178
+ const rel = path28.relative(root, filePath).replace(/\\/g, "/");
7179
+ if (rel === "" || rel.startsWith("../")) continue;
7180
+ if (rel.split("/").some((seg) => IGNORED_DIRS.has(seg))) return true;
7181
+ }
7182
+ return false;
7183
+ }
7184
+ var SUPPORTS_RECURSIVE_WATCH = process.platform === "darwin" || process.platform === "win32";
7185
+ function logWatchError(err) {
7186
+ process.stderr.write(
7187
+ chalk19.yellow("[watcher] fs error: ") + err.message + "\n"
7188
+ );
7189
+ }
7103
7190
  function createFsWatcher(opts) {
7104
7191
  const debounceMs = opts.debounceMs ?? 150;
7105
7192
  let debounceTimer = null;
7106
- const watcher = chokidar.watch(opts.roots, {
7107
- ignored: (filePath) => {
7108
- for (const root of opts.roots) {
7109
- const rel = path27.relative(root, filePath).replace(/\\/g, "/");
7110
- if (rel === ".git" || rel.startsWith(".git/")) return true;
7111
- }
7112
- return false;
7113
- },
7114
- ignoreInitial: true,
7115
- persistent: true,
7116
- awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 }
7117
- });
7118
- watcher.on("all", () => {
7193
+ const fire = () => {
7119
7194
  if (debounceTimer) clearTimeout(debounceTimer);
7120
7195
  debounceTimer = setTimeout(() => {
7121
7196
  debounceTimer = null;
7122
7197
  opts.onChange();
7123
7198
  }, debounceMs);
7199
+ };
7200
+ if (SUPPORTS_RECURSIVE_WATCH) {
7201
+ 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()))) {
7204
+ return;
7205
+ }
7206
+ fire();
7207
+ });
7208
+ w.on("error", logWatchError);
7209
+ return w;
7210
+ });
7211
+ return {
7212
+ stop() {
7213
+ if (debounceTimer) clearTimeout(debounceTimer);
7214
+ for (const w of watchers) {
7215
+ try {
7216
+ w.close();
7217
+ } catch {
7218
+ }
7219
+ }
7220
+ }
7221
+ };
7222
+ }
7223
+ const watcher = chokidar.watch(opts.roots, {
7224
+ ignored: (filePath) => isIgnoredWatchPath(opts.roots, filePath),
7225
+ ignoreInitial: true,
7226
+ persistent: true,
7227
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 }
7124
7228
  });
7125
- watcher.on("error", (err) => {
7126
- process.stderr.write(
7127
- chalk19.yellow("[watcher] fs error: ") + err.message + "\n"
7128
- );
7129
- });
7229
+ watcher.on("all", fire);
7230
+ watcher.on("error", logWatchError);
7130
7231
  return {
7131
7232
  stop() {
7132
7233
  if (debounceTimer) clearTimeout(debounceTimer);
@@ -7137,30 +7238,30 @@ function createFsWatcher(opts) {
7137
7238
  }
7138
7239
 
7139
7240
  // src/core/web-static.ts
7140
- import fs29 from "fs";
7141
- import path28 from "path";
7241
+ import fs32 from "fs";
7242
+ import path29 from "path";
7142
7243
  import { fileURLToPath } from "url";
7143
7244
  function resolveWebRoot() {
7144
- const entryDir = path28.dirname(process.argv[1] ?? "");
7145
- const moduleDir = path28.dirname(fileURLToPath(import.meta.url));
7245
+ const entryDir = path29.dirname(process.argv[1] ?? "");
7246
+ const moduleDir = path29.dirname(fileURLToPath(import.meta.url));
7146
7247
  const candidates = [
7147
- path28.join(entryDir, "web"),
7248
+ path29.join(entryDir, "web"),
7148
7249
  // bundled: this module is inlined into dist/<bin>.js, so dist/web is a
7149
7250
  // sibling of the bundle. Works even when argv[1] is an npm bin symlink
7150
7251
  // (which is not realpath'd, so the entryDir candidate above misses).
7151
- path28.join(moduleDir, "web"),
7252
+ path29.join(moduleDir, "web"),
7152
7253
  // dev/tsx fallback: walk up from src/core to repo root then into dist/web.
7153
- path28.resolve(moduleDir, "../../dist/web")
7254
+ path29.resolve(moduleDir, "../../dist/web")
7154
7255
  ];
7155
7256
  for (const c of candidates) {
7156
- if (fs29.existsSync(path28.join(c, "index.html"))) return c;
7257
+ if (fs32.existsSync(path29.join(c, "index.html"))) return c;
7157
7258
  }
7158
7259
  return null;
7159
7260
  }
7160
7261
 
7161
7262
  // src/core/spa-handler.ts
7162
- import fs30 from "fs";
7163
- import path29 from "path";
7263
+ import fs33 from "fs";
7264
+ import path30 from "path";
7164
7265
  var MIME = {
7165
7266
  ".html": "text/html; charset=utf-8",
7166
7267
  ".js": "application/javascript; charset=utf-8",
@@ -7174,18 +7275,18 @@ var MIME = {
7174
7275
  function readFile(root, relPath) {
7175
7276
  const clean = relPath.split("?")[0];
7176
7277
  const requested = clean === "/" ? "/index.html" : clean;
7177
- const filePath = path29.join(root, requested);
7178
- const norm = path29.normalize(filePath);
7179
- if (!norm.startsWith(path29.normalize(root))) return null;
7278
+ const filePath = path30.join(root, requested);
7279
+ const norm = path30.normalize(filePath);
7280
+ if (!norm.startsWith(path30.normalize(root))) return null;
7180
7281
  let stat;
7181
7282
  try {
7182
- stat = fs30.statSync(norm);
7283
+ stat = fs33.statSync(norm);
7183
7284
  } catch {
7184
7285
  return null;
7185
7286
  }
7186
7287
  if (!stat.isFile()) return null;
7187
- const ext = path29.extname(norm).toLowerCase();
7188
- return { body: fs30.readFileSync(norm), ext };
7288
+ const ext = path30.extname(norm).toLowerCase();
7289
+ return { body: fs33.readFileSync(norm), ext };
7189
7290
  }
7190
7291
  function serveSpa(c, webRoot) {
7191
7292
  const url = new URL(c.req.url);
@@ -7220,15 +7321,23 @@ async function startDiffServer(opts) {
7220
7321
  for (const cb of sseListeners) cb(payload);
7221
7322
  }
7222
7323
  const app = new Hono();
7223
- app.get(
7224
- "/api/context",
7225
- (c) => c.json({
7324
+ app.get("/api/context", (c) => {
7325
+ const primaryRoot = opts.repos[0]?.root;
7326
+ let headBranch;
7327
+ if (primaryRoot) {
7328
+ const head = git(["rev-parse", "--abbrev-ref", "HEAD"], primaryRoot);
7329
+ if (head.exitCode === 0 && head.stdout && head.stdout !== "HEAD") {
7330
+ headBranch = head.stdout;
7331
+ }
7332
+ }
7333
+ return c.json({
7226
7334
  mode: "review",
7227
7335
  scopeLabel: opts.scopeLabel,
7228
7336
  repos: opts.repos.map((r) => ({ name: r.name })),
7229
- readOnly: !!opts.readOnly
7230
- })
7231
- );
7337
+ readOnly: !!opts.readOnly,
7338
+ headBranch
7339
+ });
7340
+ });
7232
7341
  app.get("/api/diff", (c) => {
7233
7342
  const base = c.req.query("base") === "branch" ? "branch" : "uncommitted";
7234
7343
  try {
@@ -7419,16 +7528,6 @@ async function startCommentServer(opts) {
7419
7528
  stop: () => server.stop()
7420
7529
  };
7421
7530
  }
7422
- async function startReadOnlyDiffServer(opts) {
7423
- const server = await startDiffServer({
7424
- repos: opts.repos,
7425
- scopeLabel: opts.scopeLabel,
7426
- sessionBaseBranch: opts.sessionBaseBranch,
7427
- watchDebounceMs: opts.watchDebounceMs,
7428
- readOnly: true
7429
- });
7430
- return { url: server.url, stop: () => server.stop() };
7431
- }
7432
7531
  function formatSingleComment(c) {
7433
7532
  const bodyLines = c.body.split("\n").map((l) => `> ${l}`).join("\n");
7434
7533
  const header = c.side === "general" ? `**General review comment**` : `**${c.repo}/${c.file}** : line ${c.line} (${c.side})`;
@@ -7460,8 +7559,8 @@ function diffReviewSnapshot(snapshot, seen) {
7460
7559
  }
7461
7560
 
7462
7561
  // src/core/static-renderer.ts
7463
- import fs31 from "fs";
7464
- import path30 from "path";
7562
+ import fs34 from "fs";
7563
+ import path31 from "path";
7465
7564
  function buildDiff(specs, resolvedBase) {
7466
7565
  return {
7467
7566
  repos: specs.map((r) => ({
@@ -7477,8 +7576,8 @@ function renderStatic(opts) {
7477
7576
  if (!webRoot) {
7478
7577
  throw new Error("Could not find dist/web/. Run `npm run build` first.");
7479
7578
  }
7480
- const shellPath = path30.join(webRoot, "index.html");
7481
- let shell = fs31.readFileSync(shellPath, "utf-8");
7579
+ const shellPath = path31.join(webRoot, "index.html");
7580
+ let shell = fs34.readFileSync(shellPath, "utf-8");
7482
7581
  const uncommitted = buildDiff(opts.uncommitted, "HEAD");
7483
7582
  const branch = opts.branch ? buildDiff(opts.branch.specs, opts.branch.resolvedBase) : void 0;
7484
7583
  const initialBase = opts.initialBase ?? "uncommitted";
@@ -7527,10 +7626,10 @@ function escapeForScriptTag(json) {
7527
7626
  }
7528
7627
  function readAsset(webRoot, urlPath) {
7529
7628
  const clean = urlPath.split("?")[0].replace(/^\//, "");
7530
- const full = path30.join(webRoot, clean);
7531
- if (!path30.normalize(full).startsWith(path30.normalize(webRoot))) return null;
7629
+ const full = path31.join(webRoot, clean);
7630
+ if (!path31.normalize(full).startsWith(path31.normalize(webRoot))) return null;
7532
7631
  try {
7533
- return fs31.readFileSync(full, "utf-8");
7632
+ return fs34.readFileSync(full, "utf-8");
7534
7633
  } catch {
7535
7634
  return null;
7536
7635
  }
@@ -7540,108 +7639,85 @@ function readAsset(webRoot, urlPath) {
7540
7639
  function info(message) {
7541
7640
  process.stderr.write(message + "\n");
7542
7641
  }
7543
- function isPidAlive(pid) {
7642
+ function scopePathStem(repoSpecs) {
7643
+ return stableDiffPath(repoSpecs.map((r) => r.root));
7644
+ }
7645
+ async function runStop(repoSpecs) {
7646
+ const webUrl = readWebUrl();
7647
+ if (!webUrl) {
7648
+ info(chalk21.gray("No work web running \u2014 nothing to stop."));
7649
+ return;
7650
+ }
7651
+ const hash = stableDiffPath(repoSpecs.map((r) => r.root)).split(/[\\/]/).pop();
7544
7652
  try {
7545
- process.kill(pid, 0);
7546
- return true;
7653
+ const res = await fetch(`${webUrl}api/scopes/${hash}`, {
7654
+ method: "DELETE"
7655
+ });
7656
+ if (res.ok) {
7657
+ info(chalk21.gray("De-registered this scope from work web."));
7658
+ } else {
7659
+ info(
7660
+ chalk21.yellow(
7661
+ `work web responded ${res.status} \u2014 scope may not have been registered.`
7662
+ )
7663
+ );
7664
+ }
7665
+ } catch (err) {
7666
+ console.error(
7667
+ chalk21.red("Could not reach work web:"),
7668
+ err.message
7669
+ );
7670
+ }
7671
+ }
7672
+ function webUrlFilePath() {
7673
+ return path32.join(os9.homedir(), ".work", "web.url");
7674
+ }
7675
+ function resolveWorkBinPath(selfArgv1) {
7676
+ let real = selfArgv1;
7677
+ try {
7678
+ real = fs35.realpathSync(selfArgv1);
7547
7679
  } catch {
7548
- return false;
7549
7680
  }
7681
+ if (real.endsWith("wd-bin.js")) {
7682
+ return path32.join(path32.dirname(real), "bin.js");
7683
+ }
7684
+ return real;
7550
7685
  }
7551
- function readPid(pidPath) {
7686
+ function readWebUrl() {
7552
7687
  try {
7553
- const raw = fs32.readFileSync(pidPath, "utf-8").trim();
7554
- const n = Number(raw);
7555
- return Number.isFinite(n) && n > 0 ? n : null;
7688
+ const v = fs35.readFileSync(webUrlFilePath(), "utf-8").trim();
7689
+ return v || null;
7556
7690
  } catch {
7557
7691
  return null;
7558
7692
  }
7559
7693
  }
7560
- function spawnDaemon(extraArgs, logPath2) {
7561
- const out = fs32.openSync(logPath2, "a");
7694
+ async function ensureWorkWebRunning() {
7695
+ const existing = readWebUrl();
7696
+ if (existing) return existing;
7697
+ const workBin = resolveWorkBinPath(process.argv[1]);
7698
+ const out = fs35.openSync(
7699
+ path32.join(os9.homedir(), ".work", "web-autostart.log"),
7700
+ "a"
7701
+ );
7562
7702
  const child = childSpawn(
7563
7703
  process.execPath,
7564
- [process.argv[1], ...extraArgs, "--watch-daemon"],
7704
+ [workBin, "web", "--lean", "--no-open"],
7565
7705
  {
7566
7706
  detached: true,
7567
7707
  stdio: ["ignore", out, out],
7568
- windowsHide: true,
7569
- cwd: process.cwd()
7708
+ windowsHide: true
7709
+ // Inherit cwd doesn't matter for work web — its file-watches use
7710
+ // ~/.work paths exclusively.
7570
7711
  }
7571
7712
  );
7572
7713
  child.unref();
7573
- fs32.closeSync(out);
7574
- return child.pid ?? 0;
7575
- }
7576
- function pathsForScope(repoSpecs) {
7577
- const base = stableDiffPath(repoSpecs.map((r) => r.root));
7578
- return {
7579
- base,
7580
- pid: `${base}.pid`,
7581
- log: `${base}.log`,
7582
- url: `${base}.url`
7583
- };
7584
- }
7585
- function runStop(paths) {
7586
- const pid = readPid(paths.pid);
7587
- if (pid && isPidAlive(pid)) {
7588
- try {
7589
- process.kill(pid);
7590
- info(chalk21.gray(`Stopped watcher (PID ${pid}).`));
7591
- } catch (err) {
7592
- console.error(chalk21.red("Failed to stop watcher:"), err.message);
7593
- }
7594
- } else {
7595
- info(chalk21.gray("No watcher running for this scope."));
7596
- }
7597
- try {
7598
- fs32.unlinkSync(paths.pid);
7599
- } catch {
7600
- }
7601
- try {
7602
- fs32.unlinkSync(paths.url);
7603
- } catch {
7604
- }
7605
- }
7606
- async function runDaemon(ctx) {
7607
- fs32.writeFileSync(ctx.paths.pid, String(process.pid));
7608
- const handle = await startReadOnlyDiffServer({
7609
- repos: ctx.repoSpecs,
7610
- scopeLabel: ctx.scopeLabel,
7611
- sessionBaseBranch: ctx.scope.session?.baseBranch
7612
- });
7613
- fs32.writeFileSync(ctx.paths.url, handle.url);
7614
- info(
7615
- chalk21.gray(
7616
- `[live] watcher started, pid=${process.pid}, repos=${ctx.repoSpecs.map((r) => r.name).join(",")}, base=${ctx.base}, url=${handle.url}`
7617
- )
7618
- );
7619
- const shutdown = () => {
7620
- handle.stop();
7621
- try {
7622
- fs32.unlinkSync(ctx.paths.pid);
7623
- } catch {
7624
- }
7625
- try {
7626
- fs32.unlinkSync(ctx.paths.url);
7627
- } catch {
7628
- }
7629
- process.exit(0);
7630
- };
7631
- process.on("SIGINT", shutdown);
7632
- process.on("SIGTERM", shutdown);
7633
- await new Promise(() => {
7634
- });
7714
+ fs35.closeSync(out);
7715
+ const url = await waitForUrlFile(webUrlFilePath(), 5e3);
7716
+ return url;
7635
7717
  }
7636
7718
  async function tryRegisterWithWorkWeb(ctx, routeKind) {
7637
- const webUrlFile = path31.join(os9.homedir(), ".work", "web.url");
7638
- let webUrl;
7639
- try {
7640
- webUrl = fs32.readFileSync(webUrlFile, "utf-8").trim();
7641
- if (!webUrl) return null;
7642
- } catch {
7643
- return null;
7644
- }
7719
+ const webUrl = await ensureWorkWebRunning();
7720
+ if (!webUrl) return null;
7645
7721
  try {
7646
7722
  const res = await fetch(`${webUrl}api/scopes`, {
7647
7723
  method: "POST",
@@ -7662,42 +7738,19 @@ async function tryRegisterWithWorkWeb(ctx, routeKind) {
7662
7738
  async function runLauncher(ctx) {
7663
7739
  const webRouteUrl = await tryRegisterWithWorkWeb(ctx, "diff");
7664
7740
  if (webRouteUrl) {
7665
- info(chalk21.gray(`Opening in work web: ${webRouteUrl}`));
7741
+ info(chalk21.gray(`Opening: ${webRouteUrl}`));
7666
7742
  openUrl(webRouteUrl);
7667
7743
  return;
7668
7744
  }
7669
- const existing = readPid(ctx.paths.pid);
7670
- let url = null;
7671
- if (existing && isPidAlive(existing)) {
7672
- info(chalk21.gray(`Watcher already running (PID ${existing}).`));
7673
- try {
7674
- url = fs32.readFileSync(ctx.paths.url, "utf-8").trim();
7675
- } catch {
7676
- }
7677
- } else {
7678
- try {
7679
- fs32.unlinkSync(ctx.paths.pid);
7680
- } catch {
7681
- }
7682
- try {
7683
- fs32.unlinkSync(ctx.paths.url);
7684
- } catch {
7685
- }
7686
- const passthrough = process.argv.slice(2).filter((a) => a !== "--stop" && a !== "--watch");
7687
- const pid = spawnDaemon(passthrough, ctx.paths.log);
7688
- info(chalk21.gray(`Started watcher (PID ${pid}). Log: ${ctx.paths.log}`));
7689
- info(chalk21.gray("Stop with: wd --stop"));
7690
- url = await waitForUrlFile(ctx.paths.url, 3e3);
7691
- }
7692
- if (!url) {
7693
- console.error(
7694
- chalk21.red("Watcher did not report a URL \u2014 check the log:"),
7695
- ctx.paths.log
7696
- );
7697
- return;
7698
- }
7699
- info(chalk21.gray(`URL: ${url}`));
7700
- openUrl(url);
7745
+ console.error(
7746
+ chalk21.red("Could not start or reach work web.")
7747
+ );
7748
+ console.error(
7749
+ chalk21.gray(
7750
+ `Tail ~/.work/web-autostart.log for diagnostics, or run \`work web\` in another shell to inspect startup directly.`
7751
+ )
7752
+ );
7753
+ process.exitCode = 1;
7701
7754
  }
7702
7755
  function runStatic(ctx, initialBranch) {
7703
7756
  const uncommitted = buildRepoSpecs(ctx.scope, "HEAD");
@@ -7725,8 +7778,8 @@ function runStatic(ctx, initialBranch) {
7725
7778
  branch,
7726
7779
  initialBase: initialBranch && branch ? "branch" : "uncommitted"
7727
7780
  });
7728
- const filePath = `${ctx.paths.base}.html`;
7729
- fs32.writeFileSync(filePath, html, "utf-8");
7781
+ const filePath = `${ctx.scopeStem}.html`;
7782
+ fs35.writeFileSync(filePath, html, "utf-8");
7730
7783
  info(chalk21.gray(`Wrote ${filePath}`));
7731
7784
  openUrl(`file:///${filePath.replace(/\\/g, "/")}`);
7732
7785
  }
@@ -7735,7 +7788,7 @@ function waitForUrlFile(filePath, timeoutMs) {
7735
7788
  const start = Date.now();
7736
7789
  const tick = () => {
7737
7790
  try {
7738
- const v = fs32.readFileSync(filePath, "utf-8").trim();
7791
+ const v = fs35.readFileSync(filePath, "utf-8").trim();
7739
7792
  if (v) return resolve(v);
7740
7793
  } catch {
7741
7794
  }
@@ -7746,14 +7799,8 @@ function waitForUrlFile(filePath, timeoutMs) {
7746
7799
  });
7747
7800
  }
7748
7801
  async function tryReviewViaWorkWeb(ctx) {
7749
- const webUrlFile = path31.join(os9.homedir(), ".work", "web.url");
7750
- let webUrl;
7751
- try {
7752
- webUrl = fs32.readFileSync(webUrlFile, "utf-8").trim();
7753
- if (!webUrl) return false;
7754
- } catch {
7755
- return false;
7756
- }
7802
+ const webUrl = await ensureWorkWebRunning();
7803
+ if (!webUrl) return false;
7757
7804
  let hash;
7758
7805
  try {
7759
7806
  const res = await fetch(`${webUrl}api/scopes`, {
@@ -7942,12 +7989,7 @@ var diffCommand = {
7942
7989
  }).option("stop", {
7943
7990
  type: "boolean",
7944
7991
  default: false,
7945
- describe: "Stop the background server for this scope."
7946
- }).option("watch-daemon", {
7947
- type: "boolean",
7948
- default: false,
7949
- hidden: true,
7950
- describe: "Internal: run the foreground watcher loop."
7992
+ describe: "De-register this scope from work web. The work web server itself keeps running; use `work web --stop` to terminate the server."
7951
7993
  }).option("comments", {
7952
7994
  type: "boolean",
7953
7995
  alias: "c",
@@ -7965,7 +8007,8 @@ var diffCommand = {
7965
8007
  branch: argv.branch
7966
8008
  });
7967
8009
  const repoSpecs = buildRepoSpecs(scope, base);
7968
- const paths = pathsForScope(repoSpecs);
8010
+ const scopeStem = scopePathStem(repoSpecs);
8011
+ if (argv.stop) return runStop(repoSpecs);
7969
8012
  if (base === "HEAD") {
7970
8013
  info(chalk21.gray("Showing uncommitted changes vs HEAD."));
7971
8014
  } else {
@@ -7981,11 +8024,9 @@ var diffCommand = {
7981
8024
  base,
7982
8025
  baseSource,
7983
8026
  repoSpecs,
7984
- paths,
8027
+ scopeStem,
7985
8028
  scopeLabel
7986
8029
  };
7987
- if (argv.stop) return runStop(ctx.paths);
7988
- if (argv["watch-daemon"]) return runDaemon(ctx);
7989
8030
  if (argv.comments) return runReview(ctx);
7990
8031
  if (argv.static) return runStatic(ctx, !!argv.branch);
7991
8032
  return runLauncher(ctx);
@@ -7993,21 +8034,21 @@ var diffCommand = {
7993
8034
  };
7994
8035
 
7995
8036
  // src/commands/web.ts
7996
- import fs37 from "fs";
8037
+ import fs40 from "fs";
7997
8038
  import os13 from "os";
7998
- import path40 from "path";
8039
+ import path41 from "path";
7999
8040
  import chalk23 from "chalk";
8000
8041
 
8001
8042
  // src/core/web-server.ts
8002
- import fs36 from "fs";
8043
+ import fs39 from "fs";
8003
8044
  import os12 from "os";
8004
- import path39 from "path";
8045
+ import path40 from "path";
8005
8046
  import chalk22 from "chalk";
8006
8047
  import { Hono as Hono3 } from "hono";
8007
8048
  import { streamSSE as streamSSE3 } from "hono/streaming";
8008
8049
 
8009
8050
  // src/core/web-state.ts
8010
- import path32 from "path";
8051
+ import path33 from "path";
8011
8052
  import crypto4 from "crypto";
8012
8053
  import chokidar2 from "chokidar";
8013
8054
  function sessionIdFor(s) {
@@ -8028,7 +8069,7 @@ function subscribeSession(sessionId, onChange) {
8028
8069
  const watcher = chokidar2.watch(roots, {
8029
8070
  ignored: (filePath) => {
8030
8071
  for (const r of roots) {
8031
- const rel = path32.relative(r, filePath).replace(/\\/g, "/");
8072
+ const rel = path33.relative(r, filePath).replace(/\\/g, "/");
8032
8073
  if (rel === ".git" || rel.startsWith(".git/")) return true;
8033
8074
  }
8034
8075
  return false;
@@ -8142,24 +8183,24 @@ function disposeAllPtys() {
8142
8183
  }
8143
8184
 
8144
8185
  // src/core/comment-file-store.ts
8145
- import fs33 from "fs";
8146
- import path33 from "path";
8186
+ import fs36 from "fs";
8187
+ import path34 from "path";
8147
8188
  import os10 from "os";
8148
8189
  function commentsDir() {
8149
- return path33.join(os10.homedir(), ".work", "comments");
8190
+ return path34.join(os10.homedir(), ".work", "comments");
8150
8191
  }
8151
8192
  function ensureDir() {
8152
- fs33.mkdirSync(commentsDir(), { recursive: true });
8193
+ fs36.mkdirSync(commentsDir(), { recursive: true });
8153
8194
  }
8154
8195
  function commentsFileFor(sessionId) {
8155
- return path33.join(commentsDir(), `${sessionId}.json`);
8196
+ return path34.join(commentsDir(), `${sessionId}.json`);
8156
8197
  }
8157
8198
  function pathFor(sessionId) {
8158
8199
  return commentsFileFor(sessionId);
8159
8200
  }
8160
8201
  function readDisk(sessionId) {
8161
8202
  try {
8162
- const raw = fs33.readFileSync(pathFor(sessionId), "utf-8");
8203
+ const raw = fs36.readFileSync(pathFor(sessionId), "utf-8");
8163
8204
  const parsed = JSON.parse(raw);
8164
8205
  return Array.isArray(parsed) ? parsed : [];
8165
8206
  } catch {
@@ -8231,29 +8272,29 @@ function clearCommentStoreCache() {
8231
8272
  }
8232
8273
 
8233
8274
  // src/core/pending-delivery.ts
8234
- import fs34 from "fs";
8235
- import path34 from "path";
8275
+ import fs37 from "fs";
8276
+ import path35 from "path";
8236
8277
  function pathFor2(sessionId) {
8237
8278
  return {
8238
8279
  comments: commentsFileFor(sessionId),
8239
- delivered: path34.join(commentsDir(), `${sessionId}.delivered.json`)
8280
+ delivered: path35.join(commentsDir(), `${sessionId}.delivered.json`)
8240
8281
  };
8241
8282
  }
8242
8283
  function readJson(filePath, fallback) {
8243
8284
  try {
8244
- return JSON.parse(fs34.readFileSync(filePath, "utf8"));
8285
+ return JSON.parse(fs37.readFileSync(filePath, "utf8"));
8245
8286
  } catch {
8246
8287
  return fallback;
8247
8288
  }
8248
8289
  }
8249
8290
  function writeAtomic2(filePath, content) {
8250
- fs34.mkdirSync(path34.dirname(filePath), { recursive: true });
8291
+ fs37.mkdirSync(path35.dirname(filePath), { recursive: true });
8251
8292
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
8252
- fs34.writeFileSync(tmp, content, "utf-8");
8253
- fs34.renameSync(tmp, filePath);
8293
+ fs37.writeFileSync(tmp, content, "utf-8");
8294
+ fs37.renameSync(tmp, filePath);
8254
8295
  }
8255
8296
  function normalize(p) {
8256
- return path34.resolve(p).replace(/\\/g, "/").toLowerCase();
8297
+ return path35.resolve(p).replace(/\\/g, "/").toLowerCase();
8257
8298
  }
8258
8299
  function findSessionForCwd(cwd) {
8259
8300
  const norm = normalize(cwd);
@@ -8274,13 +8315,40 @@ function findSessionForCwd(cwd) {
8274
8315
  }
8275
8316
  return best?.session ?? null;
8276
8317
  }
8318
+ function isPendingFor(delivered) {
8319
+ return (c) => c.status === "published" && c.author === "user" && !delivered.has(c.id);
8320
+ }
8277
8321
  function readPendingForSession(sessionId) {
8278
8322
  const paths = pathFor2(sessionId);
8279
8323
  const comments = getCommentFileStore(sessionId).snapshot();
8280
8324
  const delivered = new Set(readJson(paths.delivered, []));
8281
- return comments.filter(
8282
- (c) => c.status === "published" && c.author === "user" && !delivered.has(c.id)
8325
+ return comments.filter(isPendingFor(delivered));
8326
+ }
8327
+ function scopeStoreIdsForPaths(paths) {
8328
+ const resolved = paths.map((p) => path35.resolve(p));
8329
+ const ids = /* @__PURE__ */ new Set();
8330
+ ids.add(`scope-${scopeHashFor(resolved)}`);
8331
+ for (const p of resolved) ids.add(`scope-${scopeHashFor([p])}`);
8332
+ return [...ids];
8333
+ }
8334
+ function readPendingForWorktree(session) {
8335
+ const sessionId = sessionIdFor(session);
8336
+ const delivered = new Set(
8337
+ readJson(pathFor2(sessionId).delivered, [])
8283
8338
  );
8339
+ const pending = isPendingFor(delivered);
8340
+ const seen = /* @__PURE__ */ new Set();
8341
+ const out = [];
8342
+ const collect = (storeId) => {
8343
+ for (const c of getCommentFileStore(storeId).snapshot()) {
8344
+ if (seen.has(c.id) || !pending(c)) continue;
8345
+ seen.add(c.id);
8346
+ out.push(c);
8347
+ }
8348
+ };
8349
+ collect(sessionId);
8350
+ for (const storeId of scopeStoreIdsForPaths(session.paths)) collect(storeId);
8351
+ return out;
8284
8352
  }
8285
8353
  function markDelivered(sessionId, ids) {
8286
8354
  if (ids.length === 0) return;
@@ -8580,8 +8648,8 @@ function mountPanesRoutes(app, opts) {
8580
8648
  }
8581
8649
 
8582
8650
  // src/core/worktree-routes.ts
8583
- import path35 from "path";
8584
- import { spawn as spawn9 } from "child_process";
8651
+ import path36 from "path";
8652
+ import { spawn as spawn10 } from "child_process";
8585
8653
  import { zValidator as zValidator4 } from "@hono/zod-validator";
8586
8654
  import { z as z3 } from "zod";
8587
8655
  function mountWorktreeRoutes(app, opts) {
@@ -8704,10 +8772,10 @@ function mountWorktreeRoutes(app, opts) {
8704
8772
  const id = c.req.param("id");
8705
8773
  const session = findSession2(id);
8706
8774
  if (!session) return c.json({ error: "unknown session" }, 404);
8707
- const target = session.isGroup ? path35.dirname(session.paths[0]) : session.paths[0];
8775
+ const target = session.isGroup ? path36.dirname(session.paths[0]) : session.paths[0];
8708
8776
  try {
8709
8777
  const cmd = process.platform === "win32" ? "code.cmd" : "code";
8710
- const child = spawn9(cmd, [target], {
8778
+ const child = spawn10(cmd, [target], {
8711
8779
  detached: true,
8712
8780
  stdio: "ignore",
8713
8781
  shell: false
@@ -8724,28 +8792,27 @@ function mountWorktreeRoutes(app, opts) {
8724
8792
  import { zValidator as zValidator5 } from "@hono/zod-validator";
8725
8793
  import { z as z4 } from "zod";
8726
8794
  import { EventEmitter } from "events";
8727
- import path38 from "path";
8728
- import spawn11 from "cross-spawn";
8795
+ import path39 from "path";
8796
+ import spawn12 from "cross-spawn";
8729
8797
 
8730
8798
  // src/core/checkpoint.ts
8731
- import fs35 from "fs";
8799
+ import fs38 from "fs";
8732
8800
  import os11 from "os";
8733
- import path36 from "path";
8734
- import crypto5 from "crypto";
8735
- import spawn10 from "cross-spawn";
8801
+ import path37 from "path";
8802
+ import spawn11 from "cross-spawn";
8736
8803
  function manifestPath(scopeHash) {
8737
- const dir = path36.join(os11.homedir(), ".work", "diffs");
8738
- fs35.mkdirSync(dir, { recursive: true });
8739
- return path36.join(dir, `${scopeHash}.checkpoints.json`);
8804
+ const dir = path37.join(os11.homedir(), ".work", "diffs");
8805
+ fs38.mkdirSync(dir, { recursive: true });
8806
+ return path37.join(dir, `${scopeHash}.checkpoints.json`);
8740
8807
  }
8741
8808
  function emptyManifest(scopeHash) {
8742
8809
  return { version: 1, scopeHash, entries: [] };
8743
8810
  }
8744
8811
  function loadManifest(scopeHash) {
8745
8812
  const file = manifestPath(scopeHash);
8746
- if (!fs35.existsSync(file)) return emptyManifest(scopeHash);
8813
+ if (!fs38.existsSync(file)) return emptyManifest(scopeHash);
8747
8814
  try {
8748
- const raw = fs35.readFileSync(file, "utf-8");
8815
+ const raw = fs38.readFileSync(file, "utf-8");
8749
8816
  const parsed = JSON.parse(raw);
8750
8817
  if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
8751
8818
  return emptyManifest(scopeHash);
@@ -8759,68 +8826,37 @@ function loadManifest(scopeHash) {
8759
8826
  return emptyManifest(scopeHash);
8760
8827
  }
8761
8828
  }
8762
- function snapshotRepo(repoRoot, scopeHash, id) {
8763
- const tmpIndex = path36.join(
8764
- os11.tmpdir(),
8765
- `wd-cp-${process.pid}-${crypto5.randomBytes(6).toString("hex")}.idx`
8766
- );
8767
- const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
8768
- const run2 = (args) => spawn10.sync("git", args, {
8829
+ function snapshotRepo(repoRoot, scopeHash, id, includeWorkingTree = true) {
8830
+ const tree = writeTempTree(repoRoot, { includeWorkingTree });
8831
+ if (!tree) return null;
8832
+ const { treeSha, headSha } = tree;
8833
+ const commitArgs = ["commit-tree", treeSha, "-m", "wd checkpoint"];
8834
+ if (headSha) commitArgs.push("-p", headSha);
8835
+ const commitEnv = {
8836
+ ...process.env,
8837
+ GIT_AUTHOR_NAME: "wd",
8838
+ GIT_AUTHOR_EMAIL: "wd@local",
8839
+ GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
8840
+ GIT_COMMITTER_NAME: "wd",
8841
+ GIT_COMMITTER_EMAIL: "wd@local",
8842
+ GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
8843
+ };
8844
+ const commit = spawn11.sync("git", commitArgs, {
8769
8845
  cwd: repoRoot,
8770
8846
  encoding: "utf-8",
8771
- env,
8772
- windowsHide: true,
8773
- maxBuffer: 64 * 1024 * 1024
8847
+ env: commitEnv,
8848
+ windowsHide: true
8774
8849
  });
8775
- try {
8776
- const headSha = (spawn10.sync("git", ["rev-parse", "--verify", "HEAD"], {
8777
- cwd: repoRoot,
8778
- encoding: "utf-8",
8779
- windowsHide: true
8780
- }).stdout ?? "").trim();
8781
- const hasHead = headSha.length > 0;
8782
- if (hasHead) {
8783
- const r = run2(["read-tree", "HEAD"]);
8784
- if (r.status !== 0) return null;
8785
- }
8786
- const add = run2(["add", "-A"]);
8787
- if (add.status !== 0) return null;
8788
- const wt = run2(["write-tree"]);
8789
- if (wt.status !== 0 || !wt.stdout) return null;
8790
- const treeSha = wt.stdout.trim();
8791
- const commitArgs = ["commit-tree", treeSha, "-m", "wd checkpoint"];
8792
- if (hasHead) commitArgs.push("-p", headSha);
8793
- const commitEnv = {
8794
- ...process.env,
8795
- GIT_AUTHOR_NAME: "wd",
8796
- GIT_AUTHOR_EMAIL: "wd@local",
8797
- GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
8798
- GIT_COMMITTER_NAME: "wd",
8799
- GIT_COMMITTER_EMAIL: "wd@local",
8800
- GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
8801
- };
8802
- const commit = spawn10.sync("git", commitArgs, {
8803
- cwd: repoRoot,
8804
- encoding: "utf-8",
8805
- env: commitEnv,
8806
- windowsHide: true
8807
- });
8808
- if (commit.status !== 0 || !commit.stdout) return null;
8809
- const commitSha = commit.stdout.trim();
8810
- const refName = `refs/wd/${scopeHash}/${id}`;
8811
- const updateRef = spawn10.sync(
8812
- "git",
8813
- ["update-ref", refName, commitSha],
8814
- { cwd: repoRoot, encoding: "utf-8", windowsHide: true }
8815
- );
8816
- if (updateRef.status !== 0) return null;
8817
- return commitSha;
8818
- } finally {
8819
- try {
8820
- if (fs35.existsSync(tmpIndex)) fs35.unlinkSync(tmpIndex);
8821
- } catch {
8822
- }
8823
- }
8850
+ if (commit.status !== 0 || !commit.stdout) return null;
8851
+ const commitSha = commit.stdout.trim();
8852
+ const refName = `refs/wd/${scopeHash}/${id}`;
8853
+ const updateRef = spawn11.sync("git", ["update-ref", refName, commitSha], {
8854
+ cwd: repoRoot,
8855
+ encoding: "utf-8",
8856
+ windowsHide: true
8857
+ });
8858
+ if (updateRef.status !== 0) return null;
8859
+ return commitSha;
8824
8860
  }
8825
8861
  async function takeCheckpoint(scopeHash, repos, opts = {}) {
8826
8862
  const file = manifestPath(scopeHash);
@@ -8831,12 +8867,17 @@ async function takeCheckpoint(scopeHash, repos, opts = {}) {
8831
8867
  const nextId = isFirst ? 0 : manifest.entries[manifest.entries.length - 1].id + 1;
8832
8868
  const captured = {};
8833
8869
  for (const repo of repos) {
8834
- captured[repo.name] = snapshotRepo(repo.root, scopeHash, nextId);
8870
+ captured[repo.name] = snapshotRepo(
8871
+ repo.root,
8872
+ scopeHash,
8873
+ nextId,
8874
+ !isFirst
8875
+ );
8835
8876
  }
8836
8877
  const rollbackRefs = () => {
8837
8878
  const refName = `refs/wd/${scopeHash}/${nextId}`;
8838
8879
  for (const repo of repos) {
8839
- spawn10.sync("git", ["update-ref", "-d", refName], {
8880
+ spawn11.sync("git", ["update-ref", "-d", refName], {
8840
8881
  cwd: repo.root,
8841
8882
  encoding: "utf-8",
8842
8883
  windowsHide: true
@@ -8849,10 +8890,22 @@ async function takeCheckpoint(scopeHash, repos, opts = {}) {
8849
8890
  }
8850
8891
  if (!opts.force && !isFirst) {
8851
8892
  const prev = manifest.entries[manifest.entries.length - 1];
8852
- const allMatch = Object.keys(captured).every(
8853
- (k) => captured[k] === prev.repos[k]
8854
- );
8855
- if (allMatch) {
8893
+ const treeOf = (root, commitSha) => {
8894
+ if (!commitSha) return null;
8895
+ const r = spawn11.sync(
8896
+ "git",
8897
+ ["rev-parse", `${commitSha}^{tree}`],
8898
+ { cwd: root, encoding: "utf-8", windowsHide: true }
8899
+ );
8900
+ if (r.status !== 0 || typeof r.stdout !== "string") return null;
8901
+ return r.stdout.trim() || null;
8902
+ };
8903
+ const allTreesMatch = repos.every((repo) => {
8904
+ const curTree = treeOf(repo.root, captured[repo.name]);
8905
+ const prevTree = treeOf(repo.root, prev.repos[repo.name] ?? null);
8906
+ return curTree !== null && curTree === prevTree;
8907
+ });
8908
+ if (allTreesMatch) {
8856
8909
  rollbackRefs();
8857
8910
  return null;
8858
8911
  }
@@ -8874,7 +8927,7 @@ function clearCheckpoints(scopeHash, repoRoots) {
8874
8927
  for (const root of repoRoots) {
8875
8928
  for (const entry of manifest.entries) {
8876
8929
  const refName = `refs/wd/${scopeHash}/${entry.id}`;
8877
- spawn10.sync("git", ["update-ref", "-d", refName], {
8930
+ spawn11.sync("git", ["update-ref", "-d", refName], {
8878
8931
  cwd: root,
8879
8932
  encoding: "utf-8",
8880
8933
  windowsHide: true
@@ -8882,21 +8935,21 @@ function clearCheckpoints(scopeHash, repoRoots) {
8882
8935
  }
8883
8936
  }
8884
8937
  const file = manifestPath(scopeHash);
8885
- if (fs35.existsSync(file)) {
8938
+ if (fs38.existsSync(file)) {
8886
8939
  try {
8887
- fs35.unlinkSync(file);
8940
+ fs38.unlinkSync(file);
8888
8941
  } catch {
8889
8942
  }
8890
8943
  }
8891
8944
  }
8892
8945
 
8893
8946
  // src/core/scope-manager.ts
8894
- import path37 from "path";
8895
- import crypto6 from "crypto";
8947
+ import path38 from "path";
8948
+ import crypto5 from "crypto";
8896
8949
  var scopes = /* @__PURE__ */ new Map();
8897
8950
  function hashFor(paths) {
8898
8951
  const key = paths.slice().sort().join("|");
8899
- return crypto6.createHash("sha1").update(key).digest("hex").slice(0, 12);
8952
+ return crypto5.createHash("sha1").update(key).digest("hex").slice(0, 12);
8900
8953
  }
8901
8954
  var ScopePathRejectedError = class extends Error {
8902
8955
  constructor(rejected) {
@@ -8908,7 +8961,7 @@ var ScopePathRejectedError = class extends Error {
8908
8961
  }
8909
8962
  };
8910
8963
  function normaliseForCompare(p) {
8911
- return path37.resolve(p).replace(/\\/g, "/").toLowerCase();
8964
+ return path38.resolve(p).replace(/\\/g, "/").toLowerCase();
8912
8965
  }
8913
8966
  function rejectedPaths(normalised) {
8914
8967
  const config = loadConfig();
@@ -8926,7 +8979,7 @@ function rejectedPaths(normalised) {
8926
8979
  });
8927
8980
  }
8928
8981
  function registerScope(paths, label2) {
8929
- const normalised = paths.map((p) => path37.resolve(p));
8982
+ const normalised = paths.map((p) => path38.resolve(p));
8930
8983
  const rejected = rejectedPaths(normalised);
8931
8984
  if (rejected.length > 0) throw new ScopePathRejectedError(rejected);
8932
8985
  const hash = hashFor(normalised);
@@ -8940,7 +8993,7 @@ function registerScope(paths, label2) {
8940
8993
  const scope = {
8941
8994
  hash,
8942
8995
  paths: normalised,
8943
- label: label2 ?? path37.basename(normalised[0]),
8996
+ label: label2 ?? path38.basename(normalised[0]),
8944
8997
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8945
8998
  ended: false
8946
8999
  };
@@ -9013,7 +9066,7 @@ function mountScopeRoutes(app, opts) {
9013
9066
  function workingTreeFingerprint(paths) {
9014
9067
  const parts = [];
9015
9068
  for (const p of paths) {
9016
- const r = spawn11.sync(
9069
+ const r = spawn12.sync(
9017
9070
  "git",
9018
9071
  ["status", "--porcelain", "--no-renames", "-z"],
9019
9072
  { cwd: p, encoding: "utf-8", windowsHide: true }
@@ -9137,7 +9190,7 @@ function mountScopeRoutes(app, opts) {
9137
9190
  const fromSha = fromEntry.repos[p] ?? "HEAD";
9138
9191
  const toSha = toEntry === void 0 ? "working" : toEntry.repos[p] ?? "HEAD";
9139
9192
  return {
9140
- name: path38.basename(p),
9193
+ name: path39.basename(p),
9141
9194
  root: p,
9142
9195
  files: computeRangeDiff({ root: p, fromRef: fromSha, toRef: toSha })
9143
9196
  };
@@ -9153,13 +9206,21 @@ function mountScopeRoutes(app, opts) {
9153
9206
  }
9154
9207
  const resolved = scope.paths.map((p) => resolveRepoDiff(p, base));
9155
9208
  const repos = scope.paths.map((p, i) => ({
9156
- name: path38.basename(p),
9209
+ name: path39.basename(p),
9157
9210
  root: p,
9158
9211
  resolvedBase: resolved[i].resolvedBase,
9159
9212
  files: computeDiff({ root: p, diffArg: resolved[i].diffArg })
9160
9213
  }));
9161
9214
  const resolvedBase = resolved[0]?.resolvedBase ?? "HEAD";
9162
- return c.json({ scopeHash: scope.hash, base, resolvedBase, repos });
9215
+ const head = git(["rev-parse", "--abbrev-ref", "HEAD"], scope.paths[0]);
9216
+ const headBranch = head.exitCode === 0 && head.stdout && head.stdout !== "HEAD" ? head.stdout : void 0;
9217
+ return c.json({
9218
+ scopeHash: scope.hash,
9219
+ base,
9220
+ resolvedBase,
9221
+ headBranch,
9222
+ repos
9223
+ });
9163
9224
  } catch (err) {
9164
9225
  return c.json({ error: err.message }, 500);
9165
9226
  }
@@ -9394,14 +9455,15 @@ function sessionToWire(s) {
9394
9455
  function computeSessionDiff(s, base) {
9395
9456
  const resolved = s.paths.map((p) => resolveRepoDiff(p, base, s.baseBranch));
9396
9457
  const repos = s.paths.map((p, i) => ({
9397
- name: path39.basename(p),
9458
+ name: path40.basename(p),
9398
9459
  root: p,
9399
9460
  resolvedBase: resolved[i].resolvedBase,
9400
9461
  files: computeDiff({ root: p, diffArg: resolved[i].diffArg })
9401
9462
  }));
9402
9463
  return { repos, resolvedBase: resolved[0]?.resolvedBase ?? "HEAD" };
9403
9464
  }
9404
- async function startWebServer() {
9465
+ async function startWebServer(opts = {}) {
9466
+ const { lean = false } = opts;
9405
9467
  const webRoot = resolveWebRoot();
9406
9468
  if (!webRoot) {
9407
9469
  throw new Error(
@@ -9413,25 +9475,27 @@ async function startWebServer() {
9413
9475
  for (const cb of sseListeners) cb({ event, data });
9414
9476
  };
9415
9477
  const home = os12.homedir();
9416
- const historyPath = path39.join(home, ".work", "history.json");
9478
+ const historyPath = path40.join(home, ".work", "history.json");
9417
9479
  const onHistoryChange = () => broadcast("sessions-changed", { ts: Date.now() });
9418
- fs36.watchFile(historyPath, { interval: 1e3 }, onHistoryChange);
9419
- const tasksPath = path39.join(home, ".work", "tasks.json");
9480
+ fs39.watchFile(historyPath, { interval: 1e3 }, onHistoryChange);
9481
+ const tasksPath = path40.join(home, ".work", "tasks.json");
9420
9482
  const onTasksChange = () => broadcast("tasks-changed", { ts: Date.now() });
9421
- fs36.watchFile(tasksPath, { interval: 1e3 }, onTasksChange);
9483
+ fs39.watchFile(tasksPath, { interval: 1e3 }, onTasksChange);
9422
9484
  const projectsRoot = claudeProjectsRoot();
9423
9485
  let activityWatcher = null;
9424
- try {
9425
- if (fs36.existsSync(projectsRoot)) {
9426
- activityWatcher = createFsWatcher({
9427
- roots: [projectsRoot],
9428
- debounceMs: 250,
9429
- onChange: () => broadcast("sessions-changed", { ts: Date.now() })
9430
- });
9486
+ if (!lean) {
9487
+ try {
9488
+ if (fs39.existsSync(projectsRoot)) {
9489
+ activityWatcher = createFsWatcher({
9490
+ roots: [projectsRoot],
9491
+ debounceMs: 250,
9492
+ onChange: () => broadcast("sessions-changed", { ts: Date.now() })
9493
+ });
9494
+ }
9495
+ } catch {
9431
9496
  }
9432
- } catch {
9433
9497
  }
9434
- const decayTick = setInterval(
9498
+ const decayTick = lean ? null : setInterval(
9435
9499
  () => broadcast("sessions-changed", { ts: Date.now() }),
9436
9500
  1e4
9437
9501
  );
@@ -9496,9 +9560,9 @@ async function startWebServer() {
9496
9560
  url: handle.url,
9497
9561
  port: handle.port,
9498
9562
  stop: async () => {
9499
- fs36.unwatchFile(historyPath, onHistoryChange);
9500
- fs36.unwatchFile(tasksPath, onTasksChange);
9501
- clearInterval(decayTick);
9563
+ fs39.unwatchFile(historyPath, onHistoryChange);
9564
+ fs39.unwatchFile(tasksPath, onTasksChange);
9565
+ if (decayTick) clearInterval(decayTick);
9502
9566
  activityWatcher?.stop();
9503
9567
  disposeAllWatchers();
9504
9568
  for (const scope of listScopes()) {
@@ -9559,12 +9623,12 @@ function info2(message) {
9559
9623
  process.stderr.write(message + "\n");
9560
9624
  }
9561
9625
  function urlFilePath() {
9562
- return path40.join(os13.homedir(), ".work", "web.url");
9626
+ return path41.join(os13.homedir(), ".work", "web.url");
9563
9627
  }
9564
9628
  function pidFilePath() {
9565
- return path40.join(os13.homedir(), ".work", "web.pid");
9629
+ return path41.join(os13.homedir(), ".work", "web.pid");
9566
9630
  }
9567
- function isPidAlive2(pid) {
9631
+ function isPidAlive(pid) {
9568
9632
  try {
9569
9633
  process.kill(pid, 0);
9570
9634
  return true;
@@ -9572,9 +9636,9 @@ function isPidAlive2(pid) {
9572
9636
  return false;
9573
9637
  }
9574
9638
  }
9575
- function readPid2() {
9639
+ function readPid() {
9576
9640
  try {
9577
- const raw = fs37.readFileSync(pidFilePath(), "utf-8").trim();
9641
+ const raw = fs40.readFileSync(pidFilePath(), "utf-8").trim();
9578
9642
  const n = Number(raw);
9579
9643
  return Number.isFinite(n) && n > 0 ? n : null;
9580
9644
  } catch {
@@ -9583,7 +9647,7 @@ function readPid2() {
9583
9647
  }
9584
9648
  function readUrl() {
9585
9649
  try {
9586
- const v = fs37.readFileSync(urlFilePath(), "utf-8").trim();
9650
+ const v = fs40.readFileSync(urlFilePath(), "utf-8").trim();
9587
9651
  return v || null;
9588
9652
  } catch {
9589
9653
  return null;
@@ -9601,19 +9665,19 @@ async function pingsAlive(url, timeoutMs = 500) {
9601
9665
  }
9602
9666
  }
9603
9667
  function stopExisting() {
9604
- const pid = readPid2();
9668
+ const pid = readPid();
9605
9669
  if (!pid) {
9606
9670
  info2(chalk23.gray("No work web running."));
9607
9671
  return false;
9608
9672
  }
9609
- if (!isPidAlive2(pid)) {
9673
+ if (!isPidAlive(pid)) {
9610
9674
  info2(chalk23.gray(`Stale PID ${pid} \u2014 cleaning up.`));
9611
9675
  try {
9612
- fs37.unlinkSync(pidFilePath());
9676
+ fs40.unlinkSync(pidFilePath());
9613
9677
  } catch {
9614
9678
  }
9615
9679
  try {
9616
- fs37.unlinkSync(urlFilePath());
9680
+ fs40.unlinkSync(urlFilePath());
9617
9681
  } catch {
9618
9682
  }
9619
9683
  return false;
@@ -9622,11 +9686,11 @@ function stopExisting() {
9622
9686
  process.kill(pid);
9623
9687
  info2(chalk23.gray(`Stopped work web (PID ${pid}).`));
9624
9688
  try {
9625
- fs37.unlinkSync(pidFilePath());
9689
+ fs40.unlinkSync(pidFilePath());
9626
9690
  } catch {
9627
9691
  }
9628
9692
  try {
9629
- fs37.unlinkSync(urlFilePath());
9693
+ fs40.unlinkSync(urlFilePath());
9630
9694
  } catch {
9631
9695
  }
9632
9696
  return true;
@@ -9646,14 +9710,19 @@ var webCommand = {
9646
9710
  type: "boolean",
9647
9711
  default: false,
9648
9712
  describe: "Stop a running work web instance and exit."
9713
+ }).option("lean", {
9714
+ type: "boolean",
9715
+ default: false,
9716
+ hidden: true,
9717
+ describe: "Internal: start without dashboard-only features (Claude activity watcher + hooks). Used by `wd` when it auto-starts work web for a diff-only session."
9649
9718
  }),
9650
9719
  handler: async (argv) => {
9651
9720
  if (argv.stop) {
9652
9721
  stopExisting();
9653
9722
  process.exit(0);
9654
9723
  }
9655
- const existingPid = readPid2();
9656
- if (existingPid && isPidAlive2(existingPid)) {
9724
+ const existingPid = readPid();
9725
+ if (existingPid && isPidAlive(existingPid)) {
9657
9726
  const url = readUrl();
9658
9727
  if (url && await pingsAlive(url)) {
9659
9728
  info2(
@@ -9672,55 +9741,64 @@ var webCommand = {
9672
9741
  process.exit(1);
9673
9742
  }
9674
9743
  try {
9675
- fs37.unlinkSync(pidFilePath());
9744
+ fs40.unlinkSync(pidFilePath());
9676
9745
  } catch {
9677
9746
  }
9678
9747
  try {
9679
- fs37.unlinkSync(urlFilePath());
9748
+ fs40.unlinkSync(urlFilePath());
9680
9749
  } catch {
9681
9750
  }
9682
- const handle = await startWebServer();
9751
+ const lean = !!argv.lean || process.env.WORK_WEB_LEAN === "1";
9752
+ const handle = await startWebServer({ lean });
9683
9753
  try {
9684
- fs37.mkdirSync(path40.dirname(urlFilePath()), { recursive: true });
9685
- fs37.writeFileSync(urlFilePath(), handle.url);
9686
- fs37.writeFileSync(pidFilePath(), String(process.pid));
9754
+ fs40.mkdirSync(path41.dirname(urlFilePath()), { recursive: true });
9755
+ fs40.writeFileSync(urlFilePath(), handle.url);
9756
+ fs40.writeFileSync(pidFilePath(), String(process.pid));
9687
9757
  } catch {
9688
9758
  }
9689
- info2(chalk23.gray(`work web running at ${handle.url}`));
9759
+ info2(
9760
+ chalk23.gray(
9761
+ `work web running at ${handle.url}${lean ? " (lean \u2014 diff-only mode)" : ""}`
9762
+ )
9763
+ );
9690
9764
  info2(chalk23.gray("Press Ctrl+C to stop. Or: `work web --stop` from another shell."));
9691
9765
  if (argv.open) openUrl(handle.url);
9692
- await Promise.all([
9693
- installCommandHook({
9694
- owner: "web",
9695
- event: "UserPromptSubmit",
9696
- command: "work hook prompt-submit",
9697
- timeoutSec: 5
9698
- }),
9699
- installCommandHook({
9700
- owner: "web",
9701
- event: "Stop",
9702
- command: "work hook stop",
9703
- timeoutSec: 5
9704
- })
9705
- ]).catch(() => {
9706
- });
9766
+ if (!lean) {
9767
+ await Promise.all([
9768
+ installCommandHook({
9769
+ owner: "web",
9770
+ event: "UserPromptSubmit",
9771
+ command: "work hook prompt-submit",
9772
+ timeoutSec: 5
9773
+ }),
9774
+ installCommandHook({
9775
+ owner: "web",
9776
+ event: "Stop",
9777
+ command: "work hook stop",
9778
+ timeoutSec: 5
9779
+ })
9780
+ ]).catch(() => {
9781
+ });
9782
+ }
9707
9783
  const shutdown = () => {
9708
9784
  info2(chalk23.gray("\nStopping work web."));
9709
9785
  try {
9710
- fs37.unlinkSync(urlFilePath());
9711
- } catch {
9712
- }
9713
- try {
9714
- fs37.unlinkSync(pidFilePath());
9786
+ fs40.unlinkSync(urlFilePath());
9715
9787
  } catch {
9716
9788
  }
9717
9789
  try {
9718
- removeCommandHookSync("web", "UserPromptSubmit");
9790
+ fs40.unlinkSync(pidFilePath());
9719
9791
  } catch {
9720
9792
  }
9721
- try {
9722
- removeCommandHookSync("web", "Stop");
9723
- } catch {
9793
+ if (!lean) {
9794
+ try {
9795
+ removeCommandHookSync("web", "UserPromptSubmit");
9796
+ } catch {
9797
+ }
9798
+ try {
9799
+ removeCommandHookSync("web", "Stop");
9800
+ } catch {
9801
+ }
9724
9802
  }
9725
9803
  handle.stop();
9726
9804
  process.exit(0);
@@ -9729,11 +9807,11 @@ var webCommand = {
9729
9807
  process.on("SIGTERM", shutdown);
9730
9808
  process.on("exit", () => {
9731
9809
  try {
9732
- fs37.unlinkSync(pidFilePath());
9810
+ fs40.unlinkSync(pidFilePath());
9733
9811
  } catch {
9734
9812
  }
9735
9813
  try {
9736
- fs37.unlinkSync(urlFilePath());
9814
+ fs40.unlinkSync(urlFilePath());
9737
9815
  } catch {
9738
9816
  }
9739
9817
  });
@@ -9749,7 +9827,7 @@ function computeHookOutput(input2) {
9749
9827
  const activity = readSessionActivity(session);
9750
9828
  if (activity.state === "stale") return null;
9751
9829
  const sessionId = sessionIdFor(session);
9752
- const pending = readPendingForSession(sessionId);
9830
+ const pending = readPendingForWorktree(session);
9753
9831
  if (pending.length === 0) return null;
9754
9832
  const text = formatPendingForPrompt(pending);
9755
9833
  if (!text) return null;
@@ -9808,7 +9886,7 @@ var hookCommand = {
9808
9886
 
9809
9887
  // src/commands/run.ts
9810
9888
  import chalk24 from "chalk";
9811
- import spawn12 from "cross-spawn";
9889
+ import spawn13 from "cross-spawn";
9812
9890
 
9813
9891
  // src/core/fleet.ts
9814
9892
  function selectSessions(sessions, filter) {
@@ -9850,7 +9928,7 @@ function killAllChildren(signal = "SIGTERM") {
9850
9928
  function runInPath(unit, cmd, prefix) {
9851
9929
  const { bin, args } = shellInvocation(cmd);
9852
9930
  return new Promise((resolve) => {
9853
- const child = spawn12(bin, args, {
9931
+ const child = spawn13(bin, args, {
9854
9932
  cwd: unit.path,
9855
9933
  stdio: prefix ? ["ignore", "pipe", "pipe"] : "inherit",
9856
9934
  shell: false
@@ -10082,15 +10160,15 @@ var runCommand = {
10082
10160
  };
10083
10161
 
10084
10162
  // src/commands/broadcast.ts
10085
- import fs39 from "fs";
10163
+ import fs42 from "fs";
10086
10164
  import chalk25 from "chalk";
10087
10165
 
10088
10166
  // src/core/broadcast.ts
10089
- import crypto7 from "crypto";
10090
- import fs38 from "fs";
10167
+ import crypto6 from "crypto";
10168
+ import fs41 from "fs";
10091
10169
  function readComments(file) {
10092
10170
  try {
10093
- const parsed = JSON.parse(fs38.readFileSync(file, "utf-8"));
10171
+ const parsed = JSON.parse(fs41.readFileSync(file, "utf-8"));
10094
10172
  return Array.isArray(parsed) ? parsed : [];
10095
10173
  } catch {
10096
10174
  return [];
@@ -10098,10 +10176,10 @@ function readComments(file) {
10098
10176
  }
10099
10177
  async function appendLocked(sessionId, body) {
10100
10178
  const file = commentsFileFor(sessionId);
10101
- fs38.mkdirSync(commentsDir(), { recursive: true });
10179
+ fs41.mkdirSync(commentsDir(), { recursive: true });
10102
10180
  ensureFile(file, "[]");
10103
10181
  const comment = {
10104
- id: crypto7.randomBytes(8).toString("hex"),
10182
+ id: crypto6.randomBytes(8).toString("hex"),
10105
10183
  repo: "",
10106
10184
  file: "",
10107
10185
  line: 0,
@@ -10135,7 +10213,7 @@ async function broadcastPrompt(sessions, filter, prompt) {
10135
10213
  // src/commands/broadcast.ts
10136
10214
  function readStdin() {
10137
10215
  try {
10138
- return fs39.readFileSync(0, "utf-8");
10216
+ return fs42.readFileSync(0, "utf-8");
10139
10217
  } catch {
10140
10218
  return "";
10141
10219
  }
@@ -10210,8 +10288,8 @@ var broadcastCommand = {
10210
10288
  };
10211
10289
 
10212
10290
  // src/completions/index.ts
10213
- import fs40 from "fs";
10214
- import path41 from "path";
10291
+ import fs43 from "fs";
10292
+ import path42 from "path";
10215
10293
  function completionHandler(current, argv, done) {
10216
10294
  const rawArgs = argv._;
10217
10295
  const args = rawArgs.slice(1, -1);
@@ -10315,7 +10393,7 @@ function completeTreeRemoveList(command, args, current, config, done) {
10315
10393
  }
10316
10394
  function completeRepoBranches(alias, current, config, done) {
10317
10395
  const repoPath = config.repos[alias];
10318
- if (!repoPath || !fs40.existsSync(repoPath)) {
10396
+ if (!repoPath || !fs43.existsSync(repoPath)) {
10319
10397
  done([]);
10320
10398
  return;
10321
10399
  }
@@ -10324,19 +10402,19 @@ function completeRepoBranches(alias, current, config, done) {
10324
10402
  done(branches);
10325
10403
  }
10326
10404
  function completeGroupBranches(groupName, repoAliases, current, config, done) {
10327
- const groupDir = path41.join(config.worktreesRoot, groupName);
10328
- if (!fs40.existsSync(groupDir)) {
10405
+ const groupDir = path42.join(config.worktreesRoot, groupName);
10406
+ if (!fs43.existsSync(groupDir)) {
10329
10407
  done([]);
10330
10408
  return;
10331
10409
  }
10332
10410
  try {
10333
- const branchDirs = fs40.readdirSync(groupDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
10334
- const bdPath = path41.join(groupDir, d.name);
10411
+ const branchDirs = fs43.readdirSync(groupDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
10412
+ const bdPath = path42.join(groupDir, d.name);
10335
10413
  for (const alias of repoAliases) {
10336
10414
  const repoPath = config.repos[alias];
10337
10415
  if (!repoPath) continue;
10338
- const subPath = path41.join(bdPath, path41.basename(repoPath));
10339
- if (fs40.existsSync(subPath)) {
10416
+ const subPath = path42.join(bdPath, path42.basename(repoPath));
10417
+ if (fs43.existsSync(subPath)) {
10340
10418
  const branch = getCurrentBranch(subPath);
10341
10419
  if (branch) return branch;
10342
10420
  }
@@ -10350,7 +10428,7 @@ function completeGroupBranches(groupName, repoAliases, current, config, done) {
10350
10428
  }
10351
10429
 
10352
10430
  // src/version.ts
10353
- var VERSION = true ? "1.5.0" : "dev";
10431
+ var VERSION = true ? "1.5.1" : "dev";
10354
10432
 
10355
10433
  // src/cli.ts
10356
10434
  function showHelp() {