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