@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/README.md +158 -52
- package/dist/bin.js +578 -500
- package/dist/bin.js.map +1 -1
- package/dist/wd-bin.js +572 -494
- package/dist/wd-bin.js.map +1 -1
- package/dist/web/assets/index-8WP9kHU6.css +1 -0
- package/dist/web/assets/index-C5ddmssO.js +106 -0
- package/dist/web/assets/index-C5ddmssO.js.map +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/index-Quf37ngR.css +0 -1
- package/dist/web/assets/index-vD0svqIi.js +0 -106
- package/dist/web/assets/index-vD0svqIi.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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(
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
{
|
|
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(
|
|
3132
|
-
|
|
3133
|
-
|
|
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(
|
|
3251
|
-
|
|
3252
|
-
|
|
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
|
|
6340
|
+
import fs35 from "fs";
|
|
6322
6341
|
import os9 from "os";
|
|
6323
|
-
import
|
|
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
|
|
6329
|
-
import
|
|
6330
|
-
import
|
|
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 =
|
|
6632
|
-
const resolvedTarget =
|
|
6633
|
-
const r =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
6662
|
-
const realRoot2 =
|
|
6663
|
-
const realPath =
|
|
6664
|
-
const 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 =
|
|
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
|
-
|
|
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 =
|
|
6757
|
+
const absPath = path25.join(root, relPath);
|
|
6727
6758
|
let buffer;
|
|
6728
6759
|
try {
|
|
6729
|
-
buffer =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
6866
|
+
const result2 = spawn9.sync(
|
|
6834
6867
|
"git",
|
|
6835
|
-
|
|
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 =
|
|
6890
|
+
const result = spawn9.sync(
|
|
6855
6891
|
"git",
|
|
6856
|
-
|
|
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
|
|
6878
|
-
import
|
|
6915
|
+
import fs29 from "fs";
|
|
6916
|
+
import path26 from "path";
|
|
6879
6917
|
import os8 from "os";
|
|
6880
6918
|
import crypto2 from "crypto";
|
|
6881
|
-
function
|
|
6919
|
+
function scopeHashFor(keyPaths) {
|
|
6882
6920
|
const key = keyPaths.slice().sort().join("|");
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
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
|
|
6930
|
+
import fs30 from "fs";
|
|
6931
|
+
import path27 from "path";
|
|
6891
6932
|
import chalk18 from "chalk";
|
|
6892
6933
|
function normPath(p) {
|
|
6893
|
-
return
|
|
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:
|
|
6907
|
-
activeRepoName:
|
|
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:
|
|
6914
|
-
activeRepoName:
|
|
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(
|
|
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:
|
|
6980
|
+
repos: s.paths.map((rp) => ({ name: path27.basename(rp), root: rp })),
|
|
6929
6981
|
activeRepoName: null
|
|
6930
6982
|
};
|
|
6931
6983
|
}
|
|
6932
6984
|
}
|
|
6933
|
-
|
|
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:
|
|
6939
|
-
activeRepoName:
|
|
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
|
|
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
|
|
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("
|
|
7128
|
-
|
|
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
|
|
7143
|
-
import
|
|
7243
|
+
import fs32 from "fs";
|
|
7244
|
+
import path29 from "path";
|
|
7144
7245
|
import { fileURLToPath } from "url";
|
|
7145
7246
|
function resolveWebRoot() {
|
|
7146
|
-
const entryDir =
|
|
7147
|
-
const moduleDir =
|
|
7247
|
+
const entryDir = path29.dirname(process.argv[1] ?? "");
|
|
7248
|
+
const moduleDir = path29.dirname(fileURLToPath(import.meta.url));
|
|
7148
7249
|
const candidates = [
|
|
7149
|
-
|
|
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
|
-
|
|
7254
|
+
path29.join(moduleDir, "web"),
|
|
7154
7255
|
// dev/tsx fallback: walk up from src/core to repo root then into dist/web.
|
|
7155
|
-
|
|
7256
|
+
path29.resolve(moduleDir, "../../dist/web")
|
|
7156
7257
|
];
|
|
7157
7258
|
for (const c of candidates) {
|
|
7158
|
-
if (
|
|
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
|
|
7165
|
-
import
|
|
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 =
|
|
7180
|
-
const norm =
|
|
7181
|
-
if (!norm.startsWith(
|
|
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 =
|
|
7285
|
+
stat = fs33.statSync(norm);
|
|
7185
7286
|
} catch {
|
|
7186
7287
|
return null;
|
|
7187
7288
|
}
|
|
7188
7289
|
if (!stat.isFile()) return null;
|
|
7189
|
-
const ext =
|
|
7190
|
-
return { body:
|
|
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
|
-
|
|
7227
|
-
|
|
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
|
|
7466
|
-
import
|
|
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 =
|
|
7483
|
-
let shell =
|
|
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 =
|
|
7533
|
-
if (!
|
|
7631
|
+
const full = path31.join(webRoot, clean);
|
|
7632
|
+
if (!path31.normalize(full).startsWith(path31.normalize(webRoot))) return null;
|
|
7534
7633
|
try {
|
|
7535
|
-
return
|
|
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
|
|
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
|
-
|
|
7548
|
-
|
|
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
|
|
7688
|
+
function readWebUrl() {
|
|
7554
7689
|
try {
|
|
7555
|
-
const
|
|
7556
|
-
|
|
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
|
|
7563
|
-
const
|
|
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
|
-
[
|
|
7706
|
+
[workBin, "web", "--lean", "--no-open"],
|
|
7567
7707
|
{
|
|
7568
7708
|
detached: true,
|
|
7569
7709
|
stdio: ["ignore", out, out],
|
|
7570
|
-
windowsHide: true
|
|
7571
|
-
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
|
-
|
|
7576
|
-
|
|
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
|
|
7640
|
-
|
|
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
|
|
7743
|
+
info(chalk21.gray(`Opening: ${webRouteUrl}`));
|
|
7668
7744
|
openUrl(webRouteUrl);
|
|
7669
7745
|
return;
|
|
7670
7746
|
}
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
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.
|
|
7731
|
-
|
|
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 =
|
|
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
|
|
7752
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
|
8039
|
+
import fs40 from "fs";
|
|
7999
8040
|
import os13 from "os";
|
|
8000
|
-
import
|
|
8041
|
+
import path41 from "path";
|
|
8001
8042
|
import chalk23 from "chalk";
|
|
8002
8043
|
|
|
8003
8044
|
// src/core/web-server.ts
|
|
8004
|
-
import
|
|
8045
|
+
import fs39 from "fs";
|
|
8005
8046
|
import os12 from "os";
|
|
8006
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
8148
|
-
import
|
|
8188
|
+
import fs36 from "fs";
|
|
8189
|
+
import path34 from "path";
|
|
8149
8190
|
import os10 from "os";
|
|
8150
8191
|
function commentsDir() {
|
|
8151
|
-
return
|
|
8192
|
+
return path34.join(os10.homedir(), ".work", "comments");
|
|
8152
8193
|
}
|
|
8153
8194
|
function ensureDir() {
|
|
8154
|
-
|
|
8195
|
+
fs36.mkdirSync(commentsDir(), { recursive: true });
|
|
8155
8196
|
}
|
|
8156
8197
|
function commentsFileFor(sessionId) {
|
|
8157
|
-
return
|
|
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 =
|
|
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
|
|
8237
|
-
import
|
|
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:
|
|
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(
|
|
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
|
-
|
|
8293
|
+
fs37.mkdirSync(path35.dirname(filePath), { recursive: true });
|
|
8253
8294
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
8254
|
-
|
|
8255
|
-
|
|
8295
|
+
fs37.writeFileSync(tmp, content, "utf-8");
|
|
8296
|
+
fs37.renameSync(tmp, filePath);
|
|
8256
8297
|
}
|
|
8257
8298
|
function normalize(p) {
|
|
8258
|
-
return
|
|
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
|
-
|
|
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
|
|
8586
|
-
import { spawn as
|
|
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 ?
|
|
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 =
|
|
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
|
|
8730
|
-
import
|
|
8797
|
+
import path39 from "path";
|
|
8798
|
+
import spawn12 from "cross-spawn";
|
|
8731
8799
|
|
|
8732
8800
|
// src/core/checkpoint.ts
|
|
8733
|
-
import
|
|
8801
|
+
import fs38 from "fs";
|
|
8734
8802
|
import os11 from "os";
|
|
8735
|
-
import
|
|
8736
|
-
import
|
|
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 =
|
|
8740
|
-
|
|
8741
|
-
return
|
|
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 (!
|
|
8815
|
+
if (!fs38.existsSync(file)) return emptyManifest(scopeHash);
|
|
8749
8816
|
try {
|
|
8750
|
-
const raw =
|
|
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
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
const
|
|
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
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
8855
|
-
(
|
|
8856
|
-
|
|
8857
|
-
|
|
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
|
-
|
|
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 (
|
|
8940
|
+
if (fs38.existsSync(file)) {
|
|
8888
8941
|
try {
|
|
8889
|
-
|
|
8942
|
+
fs38.unlinkSync(file);
|
|
8890
8943
|
} catch {
|
|
8891
8944
|
}
|
|
8892
8945
|
}
|
|
8893
8946
|
}
|
|
8894
8947
|
|
|
8895
8948
|
// src/core/scope-manager.ts
|
|
8896
|
-
import
|
|
8897
|
-
import
|
|
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
|
|
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
|
|
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) =>
|
|
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 ??
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
9480
|
+
const historyPath = path40.join(home, ".work", "history.json");
|
|
9419
9481
|
const onHistoryChange = () => broadcast("sessions-changed", { ts: Date.now() });
|
|
9420
|
-
|
|
9421
|
-
const tasksPath =
|
|
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
|
-
|
|
9485
|
+
fs39.watchFile(tasksPath, { interval: 1e3 }, onTasksChange);
|
|
9424
9486
|
const projectsRoot = claudeProjectsRoot();
|
|
9425
9487
|
let activityWatcher = null;
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
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
|
-
|
|
9502
|
-
|
|
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
|
|
9628
|
+
return path41.join(os13.homedir(), ".work", "web.url");
|
|
9565
9629
|
}
|
|
9566
9630
|
function pidFilePath() {
|
|
9567
|
-
return
|
|
9631
|
+
return path41.join(os13.homedir(), ".work", "web.pid");
|
|
9568
9632
|
}
|
|
9569
|
-
function
|
|
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
|
|
9641
|
+
function readPid() {
|
|
9578
9642
|
try {
|
|
9579
|
-
const raw =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
9675
|
+
if (!isPidAlive(pid)) {
|
|
9612
9676
|
info2(chalk23.gray(`Stale PID ${pid} \u2014 cleaning up.`));
|
|
9613
9677
|
try {
|
|
9614
|
-
|
|
9678
|
+
fs40.unlinkSync(pidFilePath());
|
|
9615
9679
|
} catch {
|
|
9616
9680
|
}
|
|
9617
9681
|
try {
|
|
9618
|
-
|
|
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
|
-
|
|
9691
|
+
fs40.unlinkSync(pidFilePath());
|
|
9628
9692
|
} catch {
|
|
9629
9693
|
}
|
|
9630
9694
|
try {
|
|
9631
|
-
|
|
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 =
|
|
9658
|
-
if (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
|
-
|
|
9746
|
+
fs40.unlinkSync(pidFilePath());
|
|
9678
9747
|
} catch {
|
|
9679
9748
|
}
|
|
9680
9749
|
try {
|
|
9681
|
-
|
|
9750
|
+
fs40.unlinkSync(urlFilePath());
|
|
9682
9751
|
} catch {
|
|
9683
9752
|
}
|
|
9684
|
-
const
|
|
9753
|
+
const lean = !!argv.lean || process.env.WORK_WEB_LEAN === "1";
|
|
9754
|
+
const handle = await startWebServer({ lean });
|
|
9685
9755
|
try {
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
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(
|
|
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
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
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
|
-
|
|
9713
|
-
} catch {
|
|
9714
|
-
}
|
|
9715
|
-
try {
|
|
9716
|
-
fs37.unlinkSync(pidFilePath());
|
|
9788
|
+
fs40.unlinkSync(urlFilePath());
|
|
9717
9789
|
} catch {
|
|
9718
9790
|
}
|
|
9719
9791
|
try {
|
|
9720
|
-
|
|
9792
|
+
fs40.unlinkSync(pidFilePath());
|
|
9721
9793
|
} catch {
|
|
9722
9794
|
}
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
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
|
-
|
|
9812
|
+
fs40.unlinkSync(pidFilePath());
|
|
9735
9813
|
} catch {
|
|
9736
9814
|
}
|
|
9737
9815
|
try {
|
|
9738
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
10165
|
+
import fs42 from "fs";
|
|
10088
10166
|
import chalk25 from "chalk";
|
|
10089
10167
|
|
|
10090
10168
|
// src/core/broadcast.ts
|
|
10091
|
-
import
|
|
10092
|
-
import
|
|
10169
|
+
import crypto6 from "crypto";
|
|
10170
|
+
import fs41 from "fs";
|
|
10093
10171
|
function readComments(file) {
|
|
10094
10172
|
try {
|
|
10095
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
10181
|
+
fs41.mkdirSync(commentsDir(), { recursive: true });
|
|
10104
10182
|
ensureFile(file, "[]");
|
|
10105
10183
|
const comment = {
|
|
10106
|
-
id:
|
|
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
|
|
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
|
|
10216
|
-
import
|
|
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 || !
|
|
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 =
|
|
10330
|
-
if (!
|
|
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 =
|
|
10336
|
-
const bdPath =
|
|
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 =
|
|
10341
|
-
if (
|
|
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.
|
|
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
|
-
|
|
10453
|
-
|
|
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
|
-
|
|
10464
|
-
|
|
10541
|
+
fs44.appendFileSync(
|
|
10542
|
+
path43.join(getConfigDir(), "debug.log"),
|
|
10465
10543
|
`${(/* @__PURE__ */ new Date()).toISOString()} [FATAL] handleFatalError: ${msg}
|
|
10466
10544
|
`
|
|
10467
10545
|
);
|