@moberg_hr/work-tree 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -54
- package/dist/bin.js +804 -530
- package/dist/bin.js.map +1 -1
- package/dist/wd-bin.js +798 -524
- package/dist/wd-bin.js.map +1 -1
- package/dist/web/assets/index-BV5-pKcI.js +106 -0
- package/dist/web/assets/index-BV5-pKcI.js.map +1 -0
- package/dist/web/assets/index-uJQw1Itb.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- 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/wd-bin.js
CHANGED
|
@@ -307,10 +307,15 @@ function fetchRemote(cwd) {
|
|
|
307
307
|
}
|
|
308
308
|
async function fetchRemoteAsync(cwd) {
|
|
309
309
|
await new Promise((resolve, reject) => {
|
|
310
|
-
execFile(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
execFile(
|
|
311
|
+
"git",
|
|
312
|
+
["fetch", "--quiet"],
|
|
313
|
+
{ cwd, timeout: 3e4, windowsHide: true },
|
|
314
|
+
(err) => {
|
|
315
|
+
if (err) reject(err);
|
|
316
|
+
else resolve();
|
|
317
|
+
}
|
|
318
|
+
);
|
|
314
319
|
});
|
|
315
320
|
if (!getDefaultBranch(cwd)) {
|
|
316
321
|
git(["remote", "set-head", "origin", "--auto"], cwd);
|
|
@@ -409,7 +414,8 @@ function generateGroupClaudeMd(groupName, repoAliases, config) {
|
|
|
409
414
|
const result = spawn2.sync("claude", ["-p"], {
|
|
410
415
|
input: prompt,
|
|
411
416
|
encoding: "utf-8",
|
|
412
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
417
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
418
|
+
windowsHide: true
|
|
413
419
|
});
|
|
414
420
|
let content;
|
|
415
421
|
if (result.status !== 0 || !result.stdout?.trim()) {
|
|
@@ -871,7 +877,11 @@ function getWindowsDocumentsFolder() {
|
|
|
871
877
|
const result = spawn4.sync(
|
|
872
878
|
exe,
|
|
873
879
|
["-NoProfile", "-Command", "[Environment]::GetFolderPath('MyDocuments')"],
|
|
874
|
-
{
|
|
880
|
+
{
|
|
881
|
+
encoding: "utf-8",
|
|
882
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
883
|
+
windowsHide: true
|
|
884
|
+
}
|
|
875
885
|
);
|
|
876
886
|
if (result.status === 0 && result.stdout) {
|
|
877
887
|
const dir = result.stdout.toString().trim();
|
|
@@ -884,7 +894,8 @@ function executableExists(name) {
|
|
|
884
894
|
const cmd = process.platform === "win32" ? "where" : "which";
|
|
885
895
|
const result = spawn4.sync(cmd, [name], {
|
|
886
896
|
encoding: "utf-8",
|
|
887
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
897
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
898
|
+
windowsHide: true
|
|
888
899
|
});
|
|
889
900
|
return result.status === 0;
|
|
890
901
|
}
|
|
@@ -1562,11 +1573,12 @@ async function upsertSession(target, isGroup, branch, paths, jiraKey, baseBranch
|
|
|
1562
1573
|
saveHistory(sessions);
|
|
1563
1574
|
});
|
|
1564
1575
|
}
|
|
1565
|
-
async function upsertSessionWithPort(target, isGroup, branch, paths, config, jiraKey, baseBranch) {
|
|
1576
|
+
async function upsertSessionWithPort(target, isGroup, branch, paths, config, jiraKey, baseBranch, baseBranches) {
|
|
1566
1577
|
return withHistoryLock(async () => {
|
|
1567
1578
|
const sessions = loadHistory();
|
|
1568
1579
|
const existing = findSession(sessions, target, branch);
|
|
1569
1580
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1581
|
+
const hasPerRepo = baseBranches && Object.keys(baseBranches).length > 0;
|
|
1570
1582
|
let port = existing?.port;
|
|
1571
1583
|
if (port === void 0) {
|
|
1572
1584
|
const seedKey = sessionKey(target, branch);
|
|
@@ -1581,6 +1593,7 @@ async function upsertSessionWithPort(target, isGroup, branch, paths, config, jir
|
|
|
1581
1593
|
existing.lastAccessedAt = now;
|
|
1582
1594
|
if (jiraKey) existing.jiraKey = jiraKey;
|
|
1583
1595
|
if (baseBranch && !existing.baseBranch) existing.baseBranch = baseBranch;
|
|
1596
|
+
if (hasPerRepo && !existing.baseBranches) existing.baseBranches = baseBranches;
|
|
1584
1597
|
if (port !== void 0) existing.port = port;
|
|
1585
1598
|
} else {
|
|
1586
1599
|
const session = {
|
|
@@ -1593,6 +1606,7 @@ async function upsertSessionWithPort(target, isGroup, branch, paths, config, jir
|
|
|
1593
1606
|
};
|
|
1594
1607
|
if (jiraKey) session.jiraKey = jiraKey;
|
|
1595
1608
|
if (baseBranch) session.baseBranch = baseBranch;
|
|
1609
|
+
if (hasPerRepo) session.baseBranches = baseBranches;
|
|
1596
1610
|
if (port !== void 0) session.port = port;
|
|
1597
1611
|
sessions.push(session);
|
|
1598
1612
|
}
|
|
@@ -1708,6 +1722,62 @@ function copyConfigFiles(repoPath, worktreePath, patterns) {
|
|
|
1708
1722
|
}
|
|
1709
1723
|
}
|
|
1710
1724
|
|
|
1725
|
+
// src/core/base-spec.ts
|
|
1726
|
+
var BaseSpecError = class extends Error {
|
|
1727
|
+
constructor(message) {
|
|
1728
|
+
super(message);
|
|
1729
|
+
this.name = "BaseSpecError";
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
function parseBaseSpec(raw) {
|
|
1733
|
+
const spec = { perRepo: {} };
|
|
1734
|
+
if (raw === void 0) return spec;
|
|
1735
|
+
const values = Array.isArray(raw) ? raw : [raw];
|
|
1736
|
+
for (const value of values) {
|
|
1737
|
+
const v = value.trim();
|
|
1738
|
+
if (!v) continue;
|
|
1739
|
+
const eq = v.indexOf("=");
|
|
1740
|
+
if (eq === -1) {
|
|
1741
|
+
if (spec.default !== void 0 && spec.default !== v) {
|
|
1742
|
+
throw new BaseSpecError(
|
|
1743
|
+
`Conflicting default --base values: '${spec.default}' and '${v}'`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
spec.default = v;
|
|
1747
|
+
} else {
|
|
1748
|
+
const alias = v.slice(0, eq).trim();
|
|
1749
|
+
const branch = v.slice(eq + 1).trim();
|
|
1750
|
+
if (!alias || !branch) {
|
|
1751
|
+
throw new BaseSpecError(
|
|
1752
|
+
`Invalid --base '${value}'. Use 'alias=branch' or a bare 'branch'.`
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
const prior = spec.perRepo[alias];
|
|
1756
|
+
if (prior !== void 0 && prior !== branch) {
|
|
1757
|
+
throw new BaseSpecError(
|
|
1758
|
+
`Conflicting --base for '${alias}': '${prior}' and '${branch}'`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
spec.perRepo[alias] = branch;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return spec;
|
|
1765
|
+
}
|
|
1766
|
+
function baseForAlias(spec, alias) {
|
|
1767
|
+
return spec.perRepo[alias] ?? spec.default;
|
|
1768
|
+
}
|
|
1769
|
+
function isEmptyBaseSpec(spec) {
|
|
1770
|
+
return spec.default === void 0 && Object.keys(spec.perRepo).length === 0;
|
|
1771
|
+
}
|
|
1772
|
+
function baseSpecOverrideAliases(spec) {
|
|
1773
|
+
return Object.keys(spec.perRepo);
|
|
1774
|
+
}
|
|
1775
|
+
function toBaseSpec(base) {
|
|
1776
|
+
if (base === void 0) return { perRepo: {} };
|
|
1777
|
+
if (typeof base === "string") return { default: base, perRepo: {} };
|
|
1778
|
+
return base;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1711
1781
|
// src/core/worktree.ts
|
|
1712
1782
|
function createSingleWorktree(repoPath, worktreePath, branchName, config, baseBranch) {
|
|
1713
1783
|
debug("createSingleWorktree", { repoPath, worktreePath, branchName, baseBranch });
|
|
@@ -1913,8 +1983,9 @@ function removeSingleWorktree(repoPath, worktreePath, branchName, force) {
|
|
|
1913
1983
|
return false;
|
|
1914
1984
|
}
|
|
1915
1985
|
}
|
|
1916
|
-
async function setupWorktree(targetName, branchName, config,
|
|
1917
|
-
|
|
1986
|
+
async function setupWorktree(targetName, branchName, config, base, jiraKey) {
|
|
1987
|
+
const spec = toBaseSpec(base);
|
|
1988
|
+
debug("setupWorktree", { targetName, branchName, spec, jiraKey });
|
|
1918
1989
|
const target = resolveProjectTarget(targetName, config);
|
|
1919
1990
|
if (!target) {
|
|
1920
1991
|
debug("setupWorktree: target not found", targetName);
|
|
@@ -1922,27 +1993,38 @@ async function setupWorktree(targetName, branchName, config, baseBranch, jiraKey
|
|
|
1922
1993
|
}
|
|
1923
1994
|
const workTreeDirName = branchName.replace(/\//g, "-");
|
|
1924
1995
|
if (target.isGroup) {
|
|
1925
|
-
return setupGroupWorktree(target.name, target.repoAliases, branchName, workTreeDirName, config,
|
|
1996
|
+
return setupGroupWorktree(target.name, target.repoAliases, branchName, workTreeDirName, config, spec, jiraKey);
|
|
1926
1997
|
} else {
|
|
1927
|
-
return setupSingleWorktree(targetName, branchName, workTreeDirName, config,
|
|
1998
|
+
return setupSingleWorktree(targetName, branchName, workTreeDirName, config, spec, jiraKey);
|
|
1928
1999
|
}
|
|
1929
2000
|
}
|
|
1930
|
-
async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDirName, config,
|
|
2001
|
+
async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDirName, config, spec, jiraKey) {
|
|
1931
2002
|
const groupWorktreePath = path12.join(config.worktreesRoot, groupName, workTreeDirName);
|
|
1932
|
-
if (
|
|
2003
|
+
if (!isEmptyBaseSpec(spec)) {
|
|
2004
|
+
const unknownAliases = baseSpecOverrideAliases(spec).filter(
|
|
2005
|
+
(a) => !repoAliases.includes(a)
|
|
2006
|
+
);
|
|
2007
|
+
if (unknownAliases.length > 0) {
|
|
2008
|
+
console.error(
|
|
2009
|
+
`--base names repo(s) not in group '${groupName}': ${unknownAliases.join(", ")}. Group repos: ${repoAliases.join(", ")}`
|
|
2010
|
+
);
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
1933
2013
|
const missingBase = [];
|
|
1934
2014
|
const branchExists = [];
|
|
1935
2015
|
for (const alias of repoAliases) {
|
|
1936
2016
|
const repoPath = config.repos[alias];
|
|
1937
|
-
|
|
1938
|
-
|
|
2017
|
+
const repoBase = baseForAlias(spec, alias);
|
|
2018
|
+
if (!repoBase) continue;
|
|
2019
|
+
if (!localBranchExists(repoBase, repoPath) && !remoteBranchExists(repoBase, repoPath)) {
|
|
2020
|
+
missingBase.push(`${alias} (${repoBase})`);
|
|
1939
2021
|
}
|
|
1940
2022
|
if (localBranchExists(branchName, repoPath) || remoteBranchExists(branchName, repoPath)) {
|
|
1941
2023
|
branchExists.push(alias);
|
|
1942
2024
|
}
|
|
1943
2025
|
}
|
|
1944
2026
|
if (missingBase.length > 0) {
|
|
1945
|
-
console.error(`Base branch
|
|
2027
|
+
console.error(`Base branch not found in: ${missingBase.join(", ")}`);
|
|
1946
2028
|
return null;
|
|
1947
2029
|
}
|
|
1948
2030
|
if (branchExists.length > 0) {
|
|
@@ -1955,14 +2037,17 @@ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDi
|
|
|
1955
2037
|
console.log("");
|
|
1956
2038
|
fs13.mkdirSync(groupWorktreePath, { recursive: true });
|
|
1957
2039
|
const createdWorktrees = [];
|
|
2040
|
+
const baseBranches = {};
|
|
1958
2041
|
for (const alias of repoAliases) {
|
|
1959
2042
|
const repoPath = config.repos[alias];
|
|
1960
2043
|
const repoName = path12.basename(repoPath);
|
|
1961
2044
|
const subWorktreePath = path12.join(groupWorktreePath, repoName);
|
|
2045
|
+
const repoBase = baseForAlias(spec, alias);
|
|
1962
2046
|
console.log(chalk5.cyan(`[${alias}] (${repoName}):`));
|
|
1963
|
-
const success = createSingleWorktree(repoPath, subWorktreePath, branchName, config,
|
|
2047
|
+
const success = createSingleWorktree(repoPath, subWorktreePath, branchName, config, repoBase);
|
|
1964
2048
|
if (success) {
|
|
1965
2049
|
createdWorktrees.push({ repoPath, worktreePath: subWorktreePath });
|
|
2050
|
+
if (repoBase) baseBranches[subWorktreePath] = repoBase;
|
|
1966
2051
|
} else {
|
|
1967
2052
|
console.log("");
|
|
1968
2053
|
console.log(chalk5.yellow("Rolling back created worktrees due to failure..."));
|
|
@@ -1992,6 +2077,8 @@ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDi
|
|
|
1992
2077
|
console.log(chalk5.yellow(`Run 'work config regengroup ${groupName}' to generate it.`));
|
|
1993
2078
|
}
|
|
1994
2079
|
const allPaths = createdWorktrees.map((wt) => wt.worktreePath);
|
|
2080
|
+
const distinctBases = [...new Set(Object.values(baseBranches))];
|
|
2081
|
+
const representativeBase = spec.default ?? (distinctBases.length === 1 ? distinctBases[0] : void 0);
|
|
1995
2082
|
const { port } = await upsertSessionWithPort(
|
|
1996
2083
|
groupName,
|
|
1997
2084
|
true,
|
|
@@ -1999,17 +2086,26 @@ async function setupGroupWorktree(groupName, repoAliases, branchName, workTreeDi
|
|
|
1999
2086
|
allPaths,
|
|
2000
2087
|
config,
|
|
2001
2088
|
jiraKey,
|
|
2002
|
-
|
|
2089
|
+
representativeBase,
|
|
2090
|
+
baseBranches
|
|
2003
2091
|
);
|
|
2004
2092
|
console.log("");
|
|
2005
2093
|
console.log(`Branch: ${branchName}`);
|
|
2006
2094
|
if (port !== void 0) console.log(chalk5.gray(`Dev-server port: ${port}`));
|
|
2007
2095
|
return { launchDir: groupWorktreePath, paths: allPaths, isGroup: true, port };
|
|
2008
2096
|
}
|
|
2009
|
-
async function setupSingleWorktree(targetName, branchName, workTreeDirName, config,
|
|
2097
|
+
async function setupSingleWorktree(targetName, branchName, workTreeDirName, config, spec, jiraKey) {
|
|
2010
2098
|
const repoPath = config.repos[targetName];
|
|
2011
2099
|
const repoName = path12.basename(repoPath);
|
|
2012
2100
|
let workTreePath = path12.join(config.worktreesRoot, repoName, workTreeDirName);
|
|
2101
|
+
const unknownAliases = baseSpecOverrideAliases(spec).filter((a) => a !== targetName);
|
|
2102
|
+
if (unknownAliases.length > 0) {
|
|
2103
|
+
console.error(
|
|
2104
|
+
`--base names repo(s) other than '${targetName}': ${unknownAliases.join(", ")}`
|
|
2105
|
+
);
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
const baseBranch = baseForAlias(spec, targetName);
|
|
2013
2109
|
if (!fs13.existsSync(repoPath)) {
|
|
2014
2110
|
console.error(`Repository path does not exist: ${repoPath}`);
|
|
2015
2111
|
return null;
|
|
@@ -2036,7 +2132,8 @@ async function setupSingleWorktree(targetName, branchName, workTreeDirName, conf
|
|
|
2036
2132
|
[workTreePath],
|
|
2037
2133
|
config,
|
|
2038
2134
|
jiraKey,
|
|
2039
|
-
baseBranch
|
|
2135
|
+
baseBranch,
|
|
2136
|
+
baseBranch ? { [workTreePath]: baseBranch } : void 0
|
|
2040
2137
|
);
|
|
2041
2138
|
console.log(`Branch: ${branchName}`);
|
|
2042
2139
|
if (port !== void 0) console.log(chalk5.gray(`Dev-server port: ${port}`));
|
|
@@ -2108,7 +2205,7 @@ var treeCommand = {
|
|
|
2108
2205
|
type: "boolean",
|
|
2109
2206
|
default: false
|
|
2110
2207
|
}).option("base", {
|
|
2111
|
-
describe: "
|
|
2208
|
+
describe: "Base branch to fork from instead of HEAD. Repeatable. Use a bare branch (--base dev) for all repos, or alias=branch (--base backend=dev --base frontend=feat/x) for per-repo bases in a group.",
|
|
2112
2209
|
type: "string"
|
|
2113
2210
|
}).option("prompt", {
|
|
2114
2211
|
describe: "Initial prompt to send to the AI tool on startup",
|
|
@@ -2133,7 +2230,17 @@ var treeCommand = {
|
|
|
2133
2230
|
const open = argv.open;
|
|
2134
2231
|
const unsafe = argv.unsafe;
|
|
2135
2232
|
const setupOnly = argv["setup-only"];
|
|
2136
|
-
|
|
2233
|
+
let baseSpec;
|
|
2234
|
+
try {
|
|
2235
|
+
baseSpec = parseBaseSpec(argv.base);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
if (err instanceof BaseSpecError) {
|
|
2238
|
+
console.error(err.message);
|
|
2239
|
+
process.exitCode = 1;
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
throw err;
|
|
2243
|
+
}
|
|
2137
2244
|
const jiraKey = argv["jira-key"];
|
|
2138
2245
|
const promptFile = argv["prompt-file"];
|
|
2139
2246
|
let initialPrompt = argv.prompt;
|
|
@@ -2171,12 +2278,10 @@ var treeCommand = {
|
|
|
2171
2278
|
process.exitCode = 1;
|
|
2172
2279
|
return;
|
|
2173
2280
|
}
|
|
2174
|
-
if (
|
|
2281
|
+
if (!isEmptyBaseSpec(baseSpec) && !branchName) {
|
|
2175
2282
|
console.error("--base requires a branch name");
|
|
2176
2283
|
console.log(
|
|
2177
|
-
chalk6.yellow(
|
|
2178
|
-
`Usage: work tree ${targetName} <branch> --base ${baseBranch}`
|
|
2179
|
-
)
|
|
2284
|
+
chalk6.yellow(`Usage: work tree ${targetName} <branch> --base <base>`)
|
|
2180
2285
|
);
|
|
2181
2286
|
process.exitCode = 1;
|
|
2182
2287
|
return;
|
|
@@ -2224,7 +2329,7 @@ var treeCommand = {
|
|
|
2224
2329
|
}
|
|
2225
2330
|
return;
|
|
2226
2331
|
}
|
|
2227
|
-
const result = await setupWorktree(targetName, branchName, config,
|
|
2332
|
+
const result = await setupWorktree(targetName, branchName, config, baseSpec, jiraKey);
|
|
2228
2333
|
if (!result) {
|
|
2229
2334
|
process.exitCode = 1;
|
|
2230
2335
|
return;
|
|
@@ -2793,10 +2898,8 @@ function collectPrunable(config, options = {}) {
|
|
|
2793
2898
|
for (const [alias, repoPath] of Object.entries(config.repos)) {
|
|
2794
2899
|
if (!fs19.existsSync(repoPath)) continue;
|
|
2795
2900
|
if (skipAliases.has(alias)) continue;
|
|
2796
|
-
const worktrees = parseWorktreeList(repoPath);
|
|
2797
|
-
const normalizedRepoPath = path16.resolve(repoPath);
|
|
2901
|
+
const worktrees = parseWorktreeList(repoPath).slice(1);
|
|
2798
2902
|
for (const wt of worktrees) {
|
|
2799
|
-
if (path16.resolve(wt.path) === normalizedRepoPath) continue;
|
|
2800
2903
|
if (!wt.branch) continue;
|
|
2801
2904
|
if (groupCoveredKeys.has(`${alias}:${wt.branch}`)) continue;
|
|
2802
2905
|
const { merged, into, confidence } = isBranchMerged(wt.branch, repoPath);
|
|
@@ -3126,10 +3229,15 @@ import spawn7 from "cross-spawn";
|
|
|
3126
3229
|
import { execFile as execFile2 } from "child_process";
|
|
3127
3230
|
function execAsync(cmd, args, cwd, timeout) {
|
|
3128
3231
|
return new Promise((resolve, reject) => {
|
|
3129
|
-
execFile2(
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3232
|
+
execFile2(
|
|
3233
|
+
cmd,
|
|
3234
|
+
args,
|
|
3235
|
+
{ cwd, encoding: "utf-8", timeout, windowsHide: true },
|
|
3236
|
+
(err, stdout) => {
|
|
3237
|
+
if (err) reject(err);
|
|
3238
|
+
else resolve(stdout ?? "");
|
|
3239
|
+
}
|
|
3240
|
+
);
|
|
3133
3241
|
});
|
|
3134
3242
|
}
|
|
3135
3243
|
function parsePrJson(stdout, repoAlias, currentUser) {
|
|
@@ -3245,10 +3353,15 @@ async function isGhAvailable() {
|
|
|
3245
3353
|
import { execFile as execFile3 } from "child_process";
|
|
3246
3354
|
function execAsync2(cmd, args, timeout) {
|
|
3247
3355
|
return new Promise((resolve, reject) => {
|
|
3248
|
-
execFile3(
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3356
|
+
execFile3(
|
|
3357
|
+
cmd,
|
|
3358
|
+
args,
|
|
3359
|
+
{ encoding: "utf-8", timeout, windowsHide: true },
|
|
3360
|
+
(err, stdout) => {
|
|
3361
|
+
if (err) reject(err);
|
|
3362
|
+
else resolve(stdout ?? "");
|
|
3363
|
+
}
|
|
3364
|
+
);
|
|
3252
3365
|
});
|
|
3253
3366
|
}
|
|
3254
3367
|
async function isAcliAvailable() {
|
|
@@ -4663,7 +4776,7 @@ function generateSlug(summary) {
|
|
|
4663
4776
|
const child = execFile4(
|
|
4664
4777
|
"claude",
|
|
4665
4778
|
["-p", "--model", "haiku"],
|
|
4666
|
-
{ encoding: "utf-8", timeout: 1e4 },
|
|
4779
|
+
{ encoding: "utf-8", timeout: 1e4, windowsHide: true },
|
|
4667
4780
|
(err, stdout) => {
|
|
4668
4781
|
const result = stdout?.trim().toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/^-|-$/g, "");
|
|
4669
4782
|
resolve(result || fallback);
|
|
@@ -6316,18 +6429,16 @@ var hydrateCommand = {
|
|
|
6316
6429
|
};
|
|
6317
6430
|
|
|
6318
6431
|
// src/commands/diff.ts
|
|
6319
|
-
import
|
|
6432
|
+
import fs36 from "fs";
|
|
6320
6433
|
import os9 from "os";
|
|
6321
|
-
import
|
|
6434
|
+
import path33 from "path";
|
|
6322
6435
|
import { spawn as childSpawn } from "child_process";
|
|
6323
6436
|
import chalk21 from "chalk";
|
|
6324
6437
|
|
|
6325
6438
|
// src/core/diff-pipeline.ts
|
|
6326
|
-
import
|
|
6327
|
-
import
|
|
6328
|
-
import
|
|
6329
|
-
import crypto from "crypto";
|
|
6330
|
-
import spawn8 from "cross-spawn";
|
|
6439
|
+
import fs28 from "fs";
|
|
6440
|
+
import path25 from "path";
|
|
6441
|
+
import spawn9 from "cross-spawn";
|
|
6331
6442
|
|
|
6332
6443
|
// src/core/diff-parse.ts
|
|
6333
6444
|
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
|
|
@@ -6619,19 +6730,65 @@ function coverageLookup(root, relPaths) {
|
|
|
6619
6730
|
return { byPath: out, lcovMtimeMs: read.mtimeMs };
|
|
6620
6731
|
}
|
|
6621
6732
|
|
|
6733
|
+
// src/core/git-tree-snapshot.ts
|
|
6734
|
+
import fs27 from "fs";
|
|
6735
|
+
import os7 from "os";
|
|
6736
|
+
import path24 from "path";
|
|
6737
|
+
import crypto from "crypto";
|
|
6738
|
+
import spawn8 from "cross-spawn";
|
|
6739
|
+
function writeTempTree(repoRoot, opts = {}) {
|
|
6740
|
+
const includeWorkingTree = opts.includeWorkingTree ?? true;
|
|
6741
|
+
const tmpIndex = path24.join(
|
|
6742
|
+
os7.tmpdir(),
|
|
6743
|
+
`wd-tree-${process.pid}-${crypto.randomBytes(6).toString("hex")}.idx`
|
|
6744
|
+
);
|
|
6745
|
+
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
6746
|
+
const run2 = (args) => spawn8.sync("git", args, {
|
|
6747
|
+
cwd: repoRoot,
|
|
6748
|
+
encoding: "utf-8",
|
|
6749
|
+
env,
|
|
6750
|
+
windowsHide: true,
|
|
6751
|
+
maxBuffer: 64 * 1024 * 1024
|
|
6752
|
+
});
|
|
6753
|
+
try {
|
|
6754
|
+
const headSha = (spawn8.sync("git", ["rev-parse", "--verify", "HEAD"], {
|
|
6755
|
+
cwd: repoRoot,
|
|
6756
|
+
encoding: "utf-8",
|
|
6757
|
+
windowsHide: true
|
|
6758
|
+
}).stdout ?? "").trim() || null;
|
|
6759
|
+
if (headSha) {
|
|
6760
|
+
const r = run2(["read-tree", "HEAD"]);
|
|
6761
|
+
if (r.status !== 0) return null;
|
|
6762
|
+
}
|
|
6763
|
+
if (includeWorkingTree) {
|
|
6764
|
+
const add = run2(["add", "-A"]);
|
|
6765
|
+
if (add.status !== 0) return null;
|
|
6766
|
+
}
|
|
6767
|
+
const wt = run2(["write-tree"]);
|
|
6768
|
+
if (wt.status !== 0 || !wt.stdout) return null;
|
|
6769
|
+
return { treeSha: wt.stdout.trim(), headSha };
|
|
6770
|
+
} finally {
|
|
6771
|
+
try {
|
|
6772
|
+
if (fs27.existsSync(tmpIndex)) fs27.unlinkSync(tmpIndex);
|
|
6773
|
+
} catch {
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
}
|
|
6777
|
+
|
|
6622
6778
|
// src/core/diff-pipeline.ts
|
|
6779
|
+
var RENAME_DETECT = "-M";
|
|
6623
6780
|
var MARKDOWN_EXT_RE = /\.(md|markdown|mdx)$/i;
|
|
6624
6781
|
var MARKDOWN_SIZE_CAP = 256 * 1024;
|
|
6625
6782
|
function isMarkdownPath(p) {
|
|
6626
6783
|
return p !== "/dev/null" && MARKDOWN_EXT_RE.test(p);
|
|
6627
6784
|
}
|
|
6628
6785
|
function isInsideRoot(root, rel) {
|
|
6629
|
-
const resolvedRoot =
|
|
6630
|
-
const resolvedTarget =
|
|
6631
|
-
const r =
|
|
6786
|
+
const resolvedRoot = path25.resolve(root);
|
|
6787
|
+
const resolvedTarget = path25.resolve(resolvedRoot, rel);
|
|
6788
|
+
const r = path25.relative(resolvedRoot, resolvedTarget);
|
|
6632
6789
|
if (r === "") return true;
|
|
6633
6790
|
if (r.startsWith("..")) return false;
|
|
6634
|
-
if (
|
|
6791
|
+
if (path25.isAbsolute(r)) return false;
|
|
6635
6792
|
return true;
|
|
6636
6793
|
}
|
|
6637
6794
|
function readMarkdownContent(root, file, fromRef, toRef) {
|
|
@@ -6640,7 +6797,7 @@ function readMarkdownContent(root, file, fromRef, toRef) {
|
|
|
6640
6797
|
return void 0;
|
|
6641
6798
|
}
|
|
6642
6799
|
const showAt = (ref, p) => {
|
|
6643
|
-
const r =
|
|
6800
|
+
const r = spawn9.sync("git", ["show", `${ref}:${p}`], {
|
|
6644
6801
|
cwd: root,
|
|
6645
6802
|
encoding: "utf-8",
|
|
6646
6803
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -6656,12 +6813,12 @@ function readMarkdownContent(root, file, fromRef, toRef) {
|
|
|
6656
6813
|
if (file.status !== "deleted" && isMarkdownPath(file.newPath) && isInsideRoot(root, file.newPath)) {
|
|
6657
6814
|
if (toRef === "working") {
|
|
6658
6815
|
try {
|
|
6659
|
-
const absPath =
|
|
6660
|
-
const realRoot2 =
|
|
6661
|
-
const realPath =
|
|
6662
|
-
const sep =
|
|
6816
|
+
const absPath = path25.join(root, file.newPath);
|
|
6817
|
+
const realRoot2 = fs28.realpathSync(path25.resolve(root));
|
|
6818
|
+
const realPath = fs28.realpathSync(absPath);
|
|
6819
|
+
const sep = path25.sep;
|
|
6663
6820
|
if (realPath === realRoot2 || realPath.startsWith(realRoot2 + sep)) {
|
|
6664
|
-
result.after =
|
|
6821
|
+
result.after = fs28.readFileSync(absPath, "utf-8");
|
|
6665
6822
|
}
|
|
6666
6823
|
} catch {
|
|
6667
6824
|
}
|
|
@@ -6679,39 +6836,7 @@ function readMarkdownContent(root, file, fromRef, toRef) {
|
|
|
6679
6836
|
return result;
|
|
6680
6837
|
}
|
|
6681
6838
|
function workingTreeTreeSha(root) {
|
|
6682
|
-
|
|
6683
|
-
os7.tmpdir(),
|
|
6684
|
-
`wd-diff-${process.pid}-${crypto.randomBytes(6).toString("hex")}.idx`
|
|
6685
|
-
);
|
|
6686
|
-
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
6687
|
-
const run2 = (args) => spawn8.sync("git", args, {
|
|
6688
|
-
cwd: root,
|
|
6689
|
-
encoding: "utf-8",
|
|
6690
|
-
env,
|
|
6691
|
-
windowsHide: true,
|
|
6692
|
-
maxBuffer: 64 * 1024 * 1024
|
|
6693
|
-
});
|
|
6694
|
-
try {
|
|
6695
|
-
const headExists = (spawn8.sync("git", ["rev-parse", "--verify", "HEAD"], {
|
|
6696
|
-
cwd: root,
|
|
6697
|
-
encoding: "utf-8",
|
|
6698
|
-
windowsHide: true
|
|
6699
|
-
}).stdout ?? "").trim().length > 0;
|
|
6700
|
-
if (headExists) {
|
|
6701
|
-
const r = run2(["read-tree", "HEAD"]);
|
|
6702
|
-
if (r.status !== 0) return null;
|
|
6703
|
-
}
|
|
6704
|
-
const add = run2(["add", "-A"]);
|
|
6705
|
-
if (add.status !== 0) return null;
|
|
6706
|
-
const wt = run2(["write-tree"]);
|
|
6707
|
-
if (wt.status !== 0 || !wt.stdout) return null;
|
|
6708
|
-
return wt.stdout.trim();
|
|
6709
|
-
} finally {
|
|
6710
|
-
try {
|
|
6711
|
-
if (fs27.existsSync(tmpIndex)) fs27.unlinkSync(tmpIndex);
|
|
6712
|
-
} catch {
|
|
6713
|
-
}
|
|
6714
|
-
}
|
|
6839
|
+
return writeTempTree(root, { includeWorkingTree: true })?.treeSha ?? null;
|
|
6715
6840
|
}
|
|
6716
6841
|
function isBinaryContent(buffer) {
|
|
6717
6842
|
const len = Math.min(buffer.length, 8192);
|
|
@@ -6721,10 +6846,10 @@ function isBinaryContent(buffer) {
|
|
|
6721
6846
|
return false;
|
|
6722
6847
|
}
|
|
6723
6848
|
function synthesizeUntrackedDiff(root, relPath) {
|
|
6724
|
-
const absPath =
|
|
6849
|
+
const absPath = path25.join(root, relPath);
|
|
6725
6850
|
let buffer;
|
|
6726
6851
|
try {
|
|
6727
|
-
buffer =
|
|
6852
|
+
buffer = fs28.readFileSync(absPath);
|
|
6728
6853
|
} catch {
|
|
6729
6854
|
return "";
|
|
6730
6855
|
}
|
|
@@ -6760,11 +6885,13 @@ new file mode 100644
|
|
|
6760
6885
|
}
|
|
6761
6886
|
function computeDiff(opts) {
|
|
6762
6887
|
const { root, diffArg } = opts;
|
|
6763
|
-
const trackedResult =
|
|
6888
|
+
const trackedResult = spawn9.sync(
|
|
6764
6889
|
"git",
|
|
6765
6890
|
// -w (ignore-all-space) hides pure whitespace changes so a reformat
|
|
6766
|
-
// of indentation doesn't drown out the real changes.
|
|
6767
|
-
|
|
6891
|
+
// of indentation doesn't drown out the real changes. RENAME_DETECT turns
|
|
6892
|
+
// on rename detection so a `git mv` (+ edits) shows as one renamed entry
|
|
6893
|
+
// with its inline diff instead of a separate delete + add pair.
|
|
6894
|
+
["diff", "--no-color", "--no-ext-diff", "-w", RENAME_DETECT, diffArg],
|
|
6768
6895
|
{
|
|
6769
6896
|
cwd: root,
|
|
6770
6897
|
encoding: "utf-8",
|
|
@@ -6810,7 +6937,7 @@ function attachCoverage(root, files) {
|
|
|
6810
6937
|
f.coverageMtimeMs = lcovMtimeMs;
|
|
6811
6938
|
let srcMtimeMs = null;
|
|
6812
6939
|
try {
|
|
6813
|
-
srcMtimeMs =
|
|
6940
|
+
srcMtimeMs = fs28.statSync(path25.join(root, f.path)).mtimeMs;
|
|
6814
6941
|
} catch {
|
|
6815
6942
|
srcMtimeMs = null;
|
|
6816
6943
|
}
|
|
@@ -6828,9 +6955,12 @@ function computeRangeDiff(opts) {
|
|
|
6828
6955
|
}
|
|
6829
6956
|
const wtTreeSha = workingTreeTreeSha(root);
|
|
6830
6957
|
if (!wtTreeSha) return [];
|
|
6831
|
-
const result2 =
|
|
6958
|
+
const result2 = spawn9.sync(
|
|
6832
6959
|
"git",
|
|
6833
|
-
|
|
6960
|
+
// RENAME_DETECT: detect renames between the checkpoint tree and the
|
|
6961
|
+
// working tree so a rename (with or without edits) renders as one
|
|
6962
|
+
// entry, matching the HEAD-vs-working path above.
|
|
6963
|
+
["diff-tree", "-r", "-p", RENAME_DETECT, "--no-color", "--no-ext-diff", fromRef, wtTreeSha],
|
|
6834
6964
|
{
|
|
6835
6965
|
cwd: root,
|
|
6836
6966
|
encoding: "utf-8",
|
|
@@ -6849,9 +6979,11 @@ function computeRangeDiff(opts) {
|
|
|
6849
6979
|
}
|
|
6850
6980
|
return parsed;
|
|
6851
6981
|
}
|
|
6852
|
-
const result =
|
|
6982
|
+
const result = spawn9.sync(
|
|
6853
6983
|
"git",
|
|
6854
|
-
|
|
6984
|
+
// RENAME_DETECT: rename detection between two checkpoint commits (no -w
|
|
6985
|
+
// here — see the note above on why range diffs keep whitespace changes).
|
|
6986
|
+
["diff", "--no-color", "--no-ext-diff", RENAME_DETECT, fromRef, toRef],
|
|
6855
6987
|
{
|
|
6856
6988
|
cwd: root,
|
|
6857
6989
|
encoding: "utf-8",
|
|
@@ -6872,69 +7004,82 @@ function computeRangeDiff(opts) {
|
|
|
6872
7004
|
}
|
|
6873
7005
|
|
|
6874
7006
|
// src/core/repo-spec.ts
|
|
6875
|
-
import
|
|
6876
|
-
import
|
|
7007
|
+
import fs29 from "fs";
|
|
7008
|
+
import path26 from "path";
|
|
6877
7009
|
import os8 from "os";
|
|
6878
7010
|
import crypto2 from "crypto";
|
|
6879
|
-
function
|
|
7011
|
+
function scopeHashFor(keyPaths) {
|
|
6880
7012
|
const key = keyPaths.slice().sort().join("|");
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
7013
|
+
return crypto2.createHash("sha1").update(key).digest("hex").slice(0, 12);
|
|
7014
|
+
}
|
|
7015
|
+
function stableDiffPath(keyPaths) {
|
|
7016
|
+
const dir = path26.join(os8.homedir(), ".work", "diffs");
|
|
7017
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
7018
|
+
return path26.join(dir, scopeHashFor(keyPaths));
|
|
6885
7019
|
}
|
|
6886
7020
|
|
|
6887
7021
|
// src/core/diff-scope.ts
|
|
6888
|
-
import
|
|
7022
|
+
import fs30 from "fs";
|
|
7023
|
+
import path27 from "path";
|
|
6889
7024
|
import chalk18 from "chalk";
|
|
6890
7025
|
function normPath(p) {
|
|
6891
|
-
return
|
|
7026
|
+
return path27.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
7027
|
+
}
|
|
7028
|
+
function realNorm(p) {
|
|
7029
|
+
try {
|
|
7030
|
+
return normPath(fs30.realpathSync(p));
|
|
7031
|
+
} catch {
|
|
7032
|
+
return normPath(p);
|
|
7033
|
+
}
|
|
6892
7034
|
}
|
|
6893
7035
|
function resolveScope(cwd) {
|
|
6894
7036
|
const normCwd = normPath(cwd);
|
|
6895
7037
|
const sessions = loadHistory();
|
|
7038
|
+
const top = git(["rev-parse", "--show-toplevel"], cwd);
|
|
7039
|
+
const toplevel = top.exitCode === 0 && top.stdout ? top.stdout : null;
|
|
7040
|
+
const realTop = toplevel ? realNorm(toplevel) : null;
|
|
6896
7041
|
for (const s of sessions) {
|
|
6897
7042
|
for (const p of s.paths) {
|
|
6898
7043
|
const np = normPath(p);
|
|
6899
7044
|
if (normCwd === np || normCwd.startsWith(np + "/")) {
|
|
7045
|
+
if (realTop && realNorm(p) !== realTop) continue;
|
|
6900
7046
|
if (s.isGroup) {
|
|
6901
7047
|
return {
|
|
6902
7048
|
isGroup: true,
|
|
6903
7049
|
session: s,
|
|
6904
|
-
repos: s.paths.map((rp) => ({ name:
|
|
6905
|
-
activeRepoName:
|
|
7050
|
+
repos: s.paths.map((rp) => ({ name: path27.basename(rp), root: rp })),
|
|
7051
|
+
activeRepoName: path27.basename(p)
|
|
6906
7052
|
};
|
|
6907
7053
|
}
|
|
6908
7054
|
return {
|
|
6909
7055
|
isGroup: false,
|
|
6910
7056
|
session: s,
|
|
6911
|
-
repos: [{ name:
|
|
6912
|
-
activeRepoName:
|
|
7057
|
+
repos: [{ name: path27.basename(p), root: p }],
|
|
7058
|
+
activeRepoName: path27.basename(p)
|
|
6913
7059
|
};
|
|
6914
7060
|
}
|
|
6915
7061
|
}
|
|
6916
7062
|
}
|
|
6917
7063
|
for (const s of sessions) {
|
|
6918
7064
|
if (!s.isGroup || s.paths.length === 0) continue;
|
|
6919
|
-
const parents = s.paths.map((p) => normPath(
|
|
7065
|
+
const parents = s.paths.map((p) => normPath(path27.dirname(p)));
|
|
6920
7066
|
const groupRoot = parents[0];
|
|
6921
7067
|
if (!parents.every((par) => par === groupRoot)) continue;
|
|
6922
7068
|
if (normCwd === groupRoot || normCwd.startsWith(groupRoot + "/")) {
|
|
6923
7069
|
return {
|
|
6924
7070
|
isGroup: true,
|
|
6925
7071
|
session: s,
|
|
6926
|
-
repos: s.paths.map((rp) => ({ name:
|
|
7072
|
+
repos: s.paths.map((rp) => ({ name: path27.basename(rp), root: rp })),
|
|
6927
7073
|
activeRepoName: null
|
|
6928
7074
|
};
|
|
6929
7075
|
}
|
|
6930
7076
|
}
|
|
6931
|
-
|
|
6932
|
-
if (toplevel.exitCode !== 0 || !toplevel.stdout) return null;
|
|
7077
|
+
if (!toplevel) return null;
|
|
6933
7078
|
return {
|
|
6934
7079
|
isGroup: false,
|
|
6935
7080
|
session: null,
|
|
6936
|
-
repos: [{ name:
|
|
6937
|
-
activeRepoName:
|
|
7081
|
+
repos: [{ name: path27.basename(toplevel), root: toplevel }],
|
|
7082
|
+
activeRepoName: path27.basename(toplevel)
|
|
6938
7083
|
};
|
|
6939
7084
|
}
|
|
6940
7085
|
function findAnyParentBranch(cwd) {
|
|
@@ -6988,11 +7133,12 @@ function resolveBase(scope, argv) {
|
|
|
6988
7133
|
}
|
|
6989
7134
|
return { base: "HEAD", source: "default" };
|
|
6990
7135
|
}
|
|
6991
|
-
function buildRepoSpecs(scope, base) {
|
|
7136
|
+
function buildRepoSpecs(scope, base, perRepoBase) {
|
|
6992
7137
|
return scope.repos.map((r) => {
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
7138
|
+
const repoBase = perRepoBase?.[r.root] ?? base;
|
|
7139
|
+
let diffArg = repoBase;
|
|
7140
|
+
if (repoBase !== "HEAD") {
|
|
7141
|
+
const mb = git(["merge-base", repoBase, "HEAD"], r.root);
|
|
6996
7142
|
if (mb.exitCode === 0 && mb.stdout) diffArg = mb.stdout;
|
|
6997
7143
|
}
|
|
6998
7144
|
return { name: r.name, root: r.root, diffArg };
|
|
@@ -7002,13 +7148,24 @@ function resolveRepoDiff(root, base, sessionBaseBranch) {
|
|
|
7002
7148
|
if (base === "uncommitted") {
|
|
7003
7149
|
return { resolvedBase: "HEAD", diffArg: "HEAD" };
|
|
7004
7150
|
}
|
|
7005
|
-
const parent = sessionBaseBranch ?? findAnyParentBranch(root);
|
|
7151
|
+
const parent = sessionBaseBranch ?? detectParentBranch(root) ?? findAnyParentBranch(root);
|
|
7006
7152
|
if (!parent) return { resolvedBase: "HEAD", diffArg: "HEAD" };
|
|
7007
7153
|
let diffArg = "HEAD";
|
|
7008
7154
|
const mb = git(["merge-base", parent, "HEAD"], root);
|
|
7009
7155
|
if (mb.exitCode === 0 && mb.stdout) diffArg = mb.stdout;
|
|
7010
7156
|
return { resolvedBase: parent, diffArg };
|
|
7011
7157
|
}
|
|
7158
|
+
function sessionBaseForPath(root) {
|
|
7159
|
+
const norm = normPath(root);
|
|
7160
|
+
for (const s of loadHistory()) {
|
|
7161
|
+
for (const p of s.paths) {
|
|
7162
|
+
if (normPath(p) === norm) {
|
|
7163
|
+
return s.baseBranches?.[p] ?? s.baseBranch;
|
|
7164
|
+
}
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7167
|
+
return void 0;
|
|
7168
|
+
}
|
|
7012
7169
|
|
|
7013
7170
|
// src/core/comment-server.ts
|
|
7014
7171
|
import { Hono as Hono2 } from "hono";
|
|
@@ -7096,37 +7253,134 @@ import { Hono } from "hono";
|
|
|
7096
7253
|
import { serve } from "@hono/node-server";
|
|
7097
7254
|
import { streamSSE } from "hono/streaming";
|
|
7098
7255
|
|
|
7256
|
+
// src/core/file-context.ts
|
|
7257
|
+
import fs31 from "fs";
|
|
7258
|
+
import path28 from "path";
|
|
7259
|
+
import spawn10 from "cross-spawn";
|
|
7260
|
+
function readContextLines(opts) {
|
|
7261
|
+
const { root, relPath, ref } = opts;
|
|
7262
|
+
if (!relPath || !isInsideRoot(root, relPath)) return null;
|
|
7263
|
+
const start = Math.max(1, Math.floor(opts.start));
|
|
7264
|
+
const end = Math.max(start, Math.floor(opts.end));
|
|
7265
|
+
const content = !ref || ref === "working" ? readWorkingTree(root, relPath) : readAtRef(root, ref, relPath);
|
|
7266
|
+
if (content === null) return null;
|
|
7267
|
+
const all = content.split(/\r?\n/);
|
|
7268
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
7269
|
+
const totalLines = all.length;
|
|
7270
|
+
const slice = all.slice(start - 1, end);
|
|
7271
|
+
return {
|
|
7272
|
+
lines: slice,
|
|
7273
|
+
start,
|
|
7274
|
+
totalLines,
|
|
7275
|
+
eof: end >= totalLines
|
|
7276
|
+
};
|
|
7277
|
+
}
|
|
7278
|
+
function readWorkingTree(root, relPath) {
|
|
7279
|
+
try {
|
|
7280
|
+
const absPath = path28.join(root, relPath);
|
|
7281
|
+
const realRoot2 = fs31.realpathSync(path28.resolve(root));
|
|
7282
|
+
const realPath = fs31.realpathSync(absPath);
|
|
7283
|
+
const sep = path28.sep;
|
|
7284
|
+
if (realPath !== realRoot2 && !realPath.startsWith(realRoot2 + sep)) {
|
|
7285
|
+
return null;
|
|
7286
|
+
}
|
|
7287
|
+
return fs31.readFileSync(absPath, "utf-8");
|
|
7288
|
+
} catch {
|
|
7289
|
+
return null;
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7292
|
+
function readAtRef(root, ref, relPath) {
|
|
7293
|
+
const r = spawn10.sync("git", ["show", `${ref}:${relPath}`], {
|
|
7294
|
+
cwd: root,
|
|
7295
|
+
encoding: "utf-8",
|
|
7296
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
7297
|
+
windowsHide: true
|
|
7298
|
+
});
|
|
7299
|
+
if (r.status === 0 && typeof r.stdout === "string") return r.stdout;
|
|
7300
|
+
return null;
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7099
7303
|
// src/core/fs-watcher.ts
|
|
7100
|
-
import
|
|
7304
|
+
import fs32 from "fs";
|
|
7305
|
+
import path29 from "path";
|
|
7101
7306
|
import chalk19 from "chalk";
|
|
7102
7307
|
import chokidar from "chokidar";
|
|
7308
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
7309
|
+
".git",
|
|
7310
|
+
"node_modules",
|
|
7311
|
+
"bin",
|
|
7312
|
+
// .NET build output
|
|
7313
|
+
"obj",
|
|
7314
|
+
// .NET build output
|
|
7315
|
+
"dist",
|
|
7316
|
+
"build",
|
|
7317
|
+
"out",
|
|
7318
|
+
"target",
|
|
7319
|
+
// Rust / JVM
|
|
7320
|
+
".next",
|
|
7321
|
+
".nuxt",
|
|
7322
|
+
".svelte-kit",
|
|
7323
|
+
".turbo",
|
|
7324
|
+
".gradle",
|
|
7325
|
+
"coverage",
|
|
7326
|
+
".vs",
|
|
7327
|
+
".idea"
|
|
7328
|
+
]);
|
|
7329
|
+
function isIgnoredWatchPath(roots, filePath) {
|
|
7330
|
+
for (const root of roots) {
|
|
7331
|
+
const rel = path29.relative(root, filePath).replace(/\\/g, "/");
|
|
7332
|
+
if (rel === "" || rel.startsWith("../")) continue;
|
|
7333
|
+
if (rel.split("/").some((seg) => IGNORED_DIRS.has(seg))) return true;
|
|
7334
|
+
}
|
|
7335
|
+
return false;
|
|
7336
|
+
}
|
|
7337
|
+
var SUPPORTS_RECURSIVE_WATCH = process.platform === "darwin" || process.platform === "win32";
|
|
7338
|
+
function logWatchError(err) {
|
|
7339
|
+
process.stderr.write(
|
|
7340
|
+
chalk19.yellow("[watcher] fs error: ") + err.message + "\n"
|
|
7341
|
+
);
|
|
7342
|
+
}
|
|
7103
7343
|
function createFsWatcher(opts) {
|
|
7104
7344
|
const debounceMs = opts.debounceMs ?? 150;
|
|
7105
7345
|
let debounceTimer = null;
|
|
7106
|
-
const
|
|
7107
|
-
ignored: (filePath) => {
|
|
7108
|
-
for (const root of opts.roots) {
|
|
7109
|
-
const rel = path27.relative(root, filePath).replace(/\\/g, "/");
|
|
7110
|
-
if (rel === ".git" || rel.startsWith(".git/")) return true;
|
|
7111
|
-
}
|
|
7112
|
-
return false;
|
|
7113
|
-
},
|
|
7114
|
-
ignoreInitial: true,
|
|
7115
|
-
persistent: true,
|
|
7116
|
-
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 }
|
|
7117
|
-
});
|
|
7118
|
-
watcher.on("all", () => {
|
|
7346
|
+
const fire = () => {
|
|
7119
7347
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
7120
7348
|
debounceTimer = setTimeout(() => {
|
|
7121
7349
|
debounceTimer = null;
|
|
7122
7350
|
opts.onChange();
|
|
7123
7351
|
}, debounceMs);
|
|
7352
|
+
};
|
|
7353
|
+
if (SUPPORTS_RECURSIVE_WATCH) {
|
|
7354
|
+
const watchers = opts.roots.map((root) => {
|
|
7355
|
+
const w = fs32.watch(root, { recursive: true }, (_event, filename) => {
|
|
7356
|
+
if (filename && isIgnoredWatchPath([root], path29.join(root, filename.toString()))) {
|
|
7357
|
+
return;
|
|
7358
|
+
}
|
|
7359
|
+
fire();
|
|
7360
|
+
});
|
|
7361
|
+
w.on("error", logWatchError);
|
|
7362
|
+
return w;
|
|
7363
|
+
});
|
|
7364
|
+
return {
|
|
7365
|
+
stop() {
|
|
7366
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
7367
|
+
for (const w of watchers) {
|
|
7368
|
+
try {
|
|
7369
|
+
w.close();
|
|
7370
|
+
} catch {
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
};
|
|
7375
|
+
}
|
|
7376
|
+
const watcher = chokidar.watch(opts.roots, {
|
|
7377
|
+
ignored: (filePath) => isIgnoredWatchPath(opts.roots, filePath),
|
|
7378
|
+
ignoreInitial: true,
|
|
7379
|
+
persistent: true,
|
|
7380
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 }
|
|
7124
7381
|
});
|
|
7125
|
-
watcher.on("
|
|
7126
|
-
|
|
7127
|
-
chalk19.yellow("[watcher] fs error: ") + err.message + "\n"
|
|
7128
|
-
);
|
|
7129
|
-
});
|
|
7382
|
+
watcher.on("all", fire);
|
|
7383
|
+
watcher.on("error", logWatchError);
|
|
7130
7384
|
return {
|
|
7131
7385
|
stop() {
|
|
7132
7386
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -7137,30 +7391,30 @@ function createFsWatcher(opts) {
|
|
|
7137
7391
|
}
|
|
7138
7392
|
|
|
7139
7393
|
// src/core/web-static.ts
|
|
7140
|
-
import
|
|
7141
|
-
import
|
|
7394
|
+
import fs33 from "fs";
|
|
7395
|
+
import path30 from "path";
|
|
7142
7396
|
import { fileURLToPath } from "url";
|
|
7143
7397
|
function resolveWebRoot() {
|
|
7144
|
-
const entryDir =
|
|
7145
|
-
const moduleDir =
|
|
7398
|
+
const entryDir = path30.dirname(process.argv[1] ?? "");
|
|
7399
|
+
const moduleDir = path30.dirname(fileURLToPath(import.meta.url));
|
|
7146
7400
|
const candidates = [
|
|
7147
|
-
|
|
7401
|
+
path30.join(entryDir, "web"),
|
|
7148
7402
|
// bundled: this module is inlined into dist/<bin>.js, so dist/web is a
|
|
7149
7403
|
// sibling of the bundle. Works even when argv[1] is an npm bin symlink
|
|
7150
7404
|
// (which is not realpath'd, so the entryDir candidate above misses).
|
|
7151
|
-
|
|
7405
|
+
path30.join(moduleDir, "web"),
|
|
7152
7406
|
// dev/tsx fallback: walk up from src/core to repo root then into dist/web.
|
|
7153
|
-
|
|
7407
|
+
path30.resolve(moduleDir, "../../dist/web")
|
|
7154
7408
|
];
|
|
7155
7409
|
for (const c of candidates) {
|
|
7156
|
-
if (
|
|
7410
|
+
if (fs33.existsSync(path30.join(c, "index.html"))) return c;
|
|
7157
7411
|
}
|
|
7158
7412
|
return null;
|
|
7159
7413
|
}
|
|
7160
7414
|
|
|
7161
7415
|
// src/core/spa-handler.ts
|
|
7162
|
-
import
|
|
7163
|
-
import
|
|
7416
|
+
import fs34 from "fs";
|
|
7417
|
+
import path31 from "path";
|
|
7164
7418
|
var MIME = {
|
|
7165
7419
|
".html": "text/html; charset=utf-8",
|
|
7166
7420
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7174,18 +7428,18 @@ var MIME = {
|
|
|
7174
7428
|
function readFile(root, relPath) {
|
|
7175
7429
|
const clean = relPath.split("?")[0];
|
|
7176
7430
|
const requested = clean === "/" ? "/index.html" : clean;
|
|
7177
|
-
const filePath =
|
|
7178
|
-
const norm =
|
|
7179
|
-
if (!norm.startsWith(
|
|
7431
|
+
const filePath = path31.join(root, requested);
|
|
7432
|
+
const norm = path31.normalize(filePath);
|
|
7433
|
+
if (!norm.startsWith(path31.normalize(root))) return null;
|
|
7180
7434
|
let stat;
|
|
7181
7435
|
try {
|
|
7182
|
-
stat =
|
|
7436
|
+
stat = fs34.statSync(norm);
|
|
7183
7437
|
} catch {
|
|
7184
7438
|
return null;
|
|
7185
7439
|
}
|
|
7186
7440
|
if (!stat.isFile()) return null;
|
|
7187
|
-
const ext =
|
|
7188
|
-
return { body:
|
|
7441
|
+
const ext = path31.extname(norm).toLowerCase();
|
|
7442
|
+
return { body: fs34.readFileSync(norm), ext };
|
|
7189
7443
|
}
|
|
7190
7444
|
function serveSpa(c, webRoot) {
|
|
7191
7445
|
const url = new URL(c.req.url);
|
|
@@ -7220,20 +7474,32 @@ async function startDiffServer(opts) {
|
|
|
7220
7474
|
for (const cb of sseListeners) cb(payload);
|
|
7221
7475
|
}
|
|
7222
7476
|
const app = new Hono();
|
|
7223
|
-
app.get(
|
|
7224
|
-
|
|
7225
|
-
|
|
7477
|
+
app.get("/api/context", (c) => {
|
|
7478
|
+
const primaryRoot = opts.repos[0]?.root;
|
|
7479
|
+
let headBranch;
|
|
7480
|
+
if (primaryRoot) {
|
|
7481
|
+
const head = git(["rev-parse", "--abbrev-ref", "HEAD"], primaryRoot);
|
|
7482
|
+
if (head.exitCode === 0 && head.stdout && head.stdout !== "HEAD") {
|
|
7483
|
+
headBranch = head.stdout;
|
|
7484
|
+
}
|
|
7485
|
+
}
|
|
7486
|
+
return c.json({
|
|
7226
7487
|
mode: "review",
|
|
7227
7488
|
scopeLabel: opts.scopeLabel,
|
|
7228
7489
|
repos: opts.repos.map((r) => ({ name: r.name })),
|
|
7229
|
-
readOnly: !!opts.readOnly
|
|
7230
|
-
|
|
7231
|
-
|
|
7490
|
+
readOnly: !!opts.readOnly,
|
|
7491
|
+
headBranch
|
|
7492
|
+
});
|
|
7493
|
+
});
|
|
7232
7494
|
app.get("/api/diff", (c) => {
|
|
7233
7495
|
const base = c.req.query("base") === "branch" ? "branch" : "uncommitted";
|
|
7234
7496
|
try {
|
|
7235
7497
|
const resolved = opts.repos.map(
|
|
7236
|
-
(r) => base === "uncommitted" ? { resolvedBase: "HEAD", diffArg: r.diffArg } : resolveRepoDiff(
|
|
7498
|
+
(r) => base === "uncommitted" ? { resolvedBase: "HEAD", diffArg: r.diffArg } : resolveRepoDiff(
|
|
7499
|
+
r.root,
|
|
7500
|
+
"branch",
|
|
7501
|
+
opts.sessionBaseBranches?.[r.root] ?? opts.sessionBaseBranch
|
|
7502
|
+
)
|
|
7237
7503
|
);
|
|
7238
7504
|
const repos = opts.repos.map((r, i) => ({
|
|
7239
7505
|
name: r.name,
|
|
@@ -7247,6 +7513,21 @@ async function startDiffServer(opts) {
|
|
|
7247
7513
|
return c.json({ error: err.message }, 500);
|
|
7248
7514
|
}
|
|
7249
7515
|
});
|
|
7516
|
+
app.get("/api/file-lines", (c) => {
|
|
7517
|
+
const repoName = c.req.query("repo") ?? "";
|
|
7518
|
+
const relPath = c.req.query("path") ?? "";
|
|
7519
|
+
const start = Number(c.req.query("start"));
|
|
7520
|
+
const end = Number(c.req.query("end"));
|
|
7521
|
+
const ref = c.req.query("ref") || void 0;
|
|
7522
|
+
if (!relPath || !Number.isInteger(start) || !Number.isInteger(end)) {
|
|
7523
|
+
return c.json({ error: "bad path/start/end" }, 400);
|
|
7524
|
+
}
|
|
7525
|
+
const root = opts.repos.length === 1 ? opts.repos[0].root : opts.repos.find((r) => r.name === repoName)?.root;
|
|
7526
|
+
if (!root) return c.json({ error: "unknown repo" }, 404);
|
|
7527
|
+
const result = readContextLines({ root, relPath, start, end, ref });
|
|
7528
|
+
if (!result) return c.json({ error: "cannot read file" }, 400);
|
|
7529
|
+
return c.json(result);
|
|
7530
|
+
});
|
|
7250
7531
|
app.get(
|
|
7251
7532
|
"/events",
|
|
7252
7533
|
(c) => streamSSE(c, async (stream) => {
|
|
@@ -7409,6 +7690,7 @@ async function startCommentServer(opts) {
|
|
|
7409
7690
|
repos: opts.repos,
|
|
7410
7691
|
scopeLabel: opts.scopeLabel,
|
|
7411
7692
|
sessionBaseBranch: opts.sessionBaseBranch,
|
|
7693
|
+
sessionBaseBranches: opts.sessionBaseBranches,
|
|
7412
7694
|
watchDebounceMs: opts.watchDebounceMs,
|
|
7413
7695
|
attachRoutes
|
|
7414
7696
|
});
|
|
@@ -7419,16 +7701,6 @@ async function startCommentServer(opts) {
|
|
|
7419
7701
|
stop: () => server.stop()
|
|
7420
7702
|
};
|
|
7421
7703
|
}
|
|
7422
|
-
async function startReadOnlyDiffServer(opts) {
|
|
7423
|
-
const server = await startDiffServer({
|
|
7424
|
-
repos: opts.repos,
|
|
7425
|
-
scopeLabel: opts.scopeLabel,
|
|
7426
|
-
sessionBaseBranch: opts.sessionBaseBranch,
|
|
7427
|
-
watchDebounceMs: opts.watchDebounceMs,
|
|
7428
|
-
readOnly: true
|
|
7429
|
-
});
|
|
7430
|
-
return { url: server.url, stop: () => server.stop() };
|
|
7431
|
-
}
|
|
7432
7704
|
function formatSingleComment(c) {
|
|
7433
7705
|
const bodyLines = c.body.split("\n").map((l) => `> ${l}`).join("\n");
|
|
7434
7706
|
const header = c.side === "general" ? `**General review comment**` : `**${c.repo}/${c.file}** : line ${c.line} (${c.side})`;
|
|
@@ -7460,8 +7732,8 @@ function diffReviewSnapshot(snapshot, seen) {
|
|
|
7460
7732
|
}
|
|
7461
7733
|
|
|
7462
7734
|
// src/core/static-renderer.ts
|
|
7463
|
-
import
|
|
7464
|
-
import
|
|
7735
|
+
import fs35 from "fs";
|
|
7736
|
+
import path32 from "path";
|
|
7465
7737
|
function buildDiff(specs, resolvedBase) {
|
|
7466
7738
|
return {
|
|
7467
7739
|
repos: specs.map((r) => ({
|
|
@@ -7477,8 +7749,8 @@ function renderStatic(opts) {
|
|
|
7477
7749
|
if (!webRoot) {
|
|
7478
7750
|
throw new Error("Could not find dist/web/. Run `npm run build` first.");
|
|
7479
7751
|
}
|
|
7480
|
-
const shellPath =
|
|
7481
|
-
let shell =
|
|
7752
|
+
const shellPath = path32.join(webRoot, "index.html");
|
|
7753
|
+
let shell = fs35.readFileSync(shellPath, "utf-8");
|
|
7482
7754
|
const uncommitted = buildDiff(opts.uncommitted, "HEAD");
|
|
7483
7755
|
const branch = opts.branch ? buildDiff(opts.branch.specs, opts.branch.resolvedBase) : void 0;
|
|
7484
7756
|
const initialBase = opts.initialBase ?? "uncommitted";
|
|
@@ -7527,10 +7799,10 @@ function escapeForScriptTag(json) {
|
|
|
7527
7799
|
}
|
|
7528
7800
|
function readAsset(webRoot, urlPath) {
|
|
7529
7801
|
const clean = urlPath.split("?")[0].replace(/^\//, "");
|
|
7530
|
-
const full =
|
|
7531
|
-
if (!
|
|
7802
|
+
const full = path32.join(webRoot, clean);
|
|
7803
|
+
if (!path32.normalize(full).startsWith(path32.normalize(webRoot))) return null;
|
|
7532
7804
|
try {
|
|
7533
|
-
return
|
|
7805
|
+
return fs35.readFileSync(full, "utf-8");
|
|
7534
7806
|
} catch {
|
|
7535
7807
|
return null;
|
|
7536
7808
|
}
|
|
@@ -7540,108 +7812,85 @@ function readAsset(webRoot, urlPath) {
|
|
|
7540
7812
|
function info(message) {
|
|
7541
7813
|
process.stderr.write(message + "\n");
|
|
7542
7814
|
}
|
|
7543
|
-
function
|
|
7815
|
+
function scopePathStem(repoSpecs) {
|
|
7816
|
+
return stableDiffPath(repoSpecs.map((r) => r.root));
|
|
7817
|
+
}
|
|
7818
|
+
async function runStop(repoSpecs) {
|
|
7819
|
+
const webUrl = readWebUrl();
|
|
7820
|
+
if (!webUrl) {
|
|
7821
|
+
info(chalk21.gray("No work web running \u2014 nothing to stop."));
|
|
7822
|
+
return;
|
|
7823
|
+
}
|
|
7824
|
+
const hash = stableDiffPath(repoSpecs.map((r) => r.root)).split(/[\\/]/).pop();
|
|
7544
7825
|
try {
|
|
7545
|
-
|
|
7546
|
-
|
|
7826
|
+
const res = await fetch(`${webUrl}api/scopes/${hash}`, {
|
|
7827
|
+
method: "DELETE"
|
|
7828
|
+
});
|
|
7829
|
+
if (res.ok) {
|
|
7830
|
+
info(chalk21.gray("De-registered this scope from work web."));
|
|
7831
|
+
} else {
|
|
7832
|
+
info(
|
|
7833
|
+
chalk21.yellow(
|
|
7834
|
+
`work web responded ${res.status} \u2014 scope may not have been registered.`
|
|
7835
|
+
)
|
|
7836
|
+
);
|
|
7837
|
+
}
|
|
7838
|
+
} catch (err) {
|
|
7839
|
+
console.error(
|
|
7840
|
+
chalk21.red("Could not reach work web:"),
|
|
7841
|
+
err.message
|
|
7842
|
+
);
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7845
|
+
function webUrlFilePath() {
|
|
7846
|
+
return path33.join(os9.homedir(), ".work", "web.url");
|
|
7847
|
+
}
|
|
7848
|
+
function resolveWorkBinPath(selfArgv1) {
|
|
7849
|
+
let real = selfArgv1;
|
|
7850
|
+
try {
|
|
7851
|
+
real = fs36.realpathSync(selfArgv1);
|
|
7547
7852
|
} catch {
|
|
7548
|
-
return false;
|
|
7549
7853
|
}
|
|
7854
|
+
if (real.endsWith("wd-bin.js")) {
|
|
7855
|
+
return path33.join(path33.dirname(real), "bin.js");
|
|
7856
|
+
}
|
|
7857
|
+
return real;
|
|
7550
7858
|
}
|
|
7551
|
-
function
|
|
7859
|
+
function readWebUrl() {
|
|
7552
7860
|
try {
|
|
7553
|
-
const
|
|
7554
|
-
|
|
7555
|
-
return Number.isFinite(n) && n > 0 ? n : null;
|
|
7861
|
+
const v = fs36.readFileSync(webUrlFilePath(), "utf-8").trim();
|
|
7862
|
+
return v || null;
|
|
7556
7863
|
} catch {
|
|
7557
7864
|
return null;
|
|
7558
7865
|
}
|
|
7559
7866
|
}
|
|
7560
|
-
function
|
|
7561
|
-
const
|
|
7867
|
+
async function ensureWorkWebRunning() {
|
|
7868
|
+
const existing = readWebUrl();
|
|
7869
|
+
if (existing) return existing;
|
|
7870
|
+
const workBin = resolveWorkBinPath(process.argv[1]);
|
|
7871
|
+
const out = fs36.openSync(
|
|
7872
|
+
path33.join(os9.homedir(), ".work", "web-autostart.log"),
|
|
7873
|
+
"a"
|
|
7874
|
+
);
|
|
7562
7875
|
const child = childSpawn(
|
|
7563
7876
|
process.execPath,
|
|
7564
|
-
[
|
|
7877
|
+
[workBin, "web", "--lean", "--no-open"],
|
|
7565
7878
|
{
|
|
7566
7879
|
detached: true,
|
|
7567
7880
|
stdio: ["ignore", out, out],
|
|
7568
|
-
windowsHide: true
|
|
7569
|
-
cwd
|
|
7881
|
+
windowsHide: true
|
|
7882
|
+
// Inherit cwd doesn't matter for work web — its file-watches use
|
|
7883
|
+
// ~/.work paths exclusively.
|
|
7570
7884
|
}
|
|
7571
7885
|
);
|
|
7572
7886
|
child.unref();
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
function pathsForScope(repoSpecs) {
|
|
7577
|
-
const base = stableDiffPath(repoSpecs.map((r) => r.root));
|
|
7578
|
-
return {
|
|
7579
|
-
base,
|
|
7580
|
-
pid: `${base}.pid`,
|
|
7581
|
-
log: `${base}.log`,
|
|
7582
|
-
url: `${base}.url`
|
|
7583
|
-
};
|
|
7584
|
-
}
|
|
7585
|
-
function runStop(paths) {
|
|
7586
|
-
const pid = readPid(paths.pid);
|
|
7587
|
-
if (pid && isPidAlive(pid)) {
|
|
7588
|
-
try {
|
|
7589
|
-
process.kill(pid);
|
|
7590
|
-
info(chalk21.gray(`Stopped watcher (PID ${pid}).`));
|
|
7591
|
-
} catch (err) {
|
|
7592
|
-
console.error(chalk21.red("Failed to stop watcher:"), err.message);
|
|
7593
|
-
}
|
|
7594
|
-
} else {
|
|
7595
|
-
info(chalk21.gray("No watcher running for this scope."));
|
|
7596
|
-
}
|
|
7597
|
-
try {
|
|
7598
|
-
fs32.unlinkSync(paths.pid);
|
|
7599
|
-
} catch {
|
|
7600
|
-
}
|
|
7601
|
-
try {
|
|
7602
|
-
fs32.unlinkSync(paths.url);
|
|
7603
|
-
} catch {
|
|
7604
|
-
}
|
|
7605
|
-
}
|
|
7606
|
-
async function runDaemon(ctx) {
|
|
7607
|
-
fs32.writeFileSync(ctx.paths.pid, String(process.pid));
|
|
7608
|
-
const handle = await startReadOnlyDiffServer({
|
|
7609
|
-
repos: ctx.repoSpecs,
|
|
7610
|
-
scopeLabel: ctx.scopeLabel,
|
|
7611
|
-
sessionBaseBranch: ctx.scope.session?.baseBranch
|
|
7612
|
-
});
|
|
7613
|
-
fs32.writeFileSync(ctx.paths.url, handle.url);
|
|
7614
|
-
info(
|
|
7615
|
-
chalk21.gray(
|
|
7616
|
-
`[live] watcher started, pid=${process.pid}, repos=${ctx.repoSpecs.map((r) => r.name).join(",")}, base=${ctx.base}, url=${handle.url}`
|
|
7617
|
-
)
|
|
7618
|
-
);
|
|
7619
|
-
const shutdown = () => {
|
|
7620
|
-
handle.stop();
|
|
7621
|
-
try {
|
|
7622
|
-
fs32.unlinkSync(ctx.paths.pid);
|
|
7623
|
-
} catch {
|
|
7624
|
-
}
|
|
7625
|
-
try {
|
|
7626
|
-
fs32.unlinkSync(ctx.paths.url);
|
|
7627
|
-
} catch {
|
|
7628
|
-
}
|
|
7629
|
-
process.exit(0);
|
|
7630
|
-
};
|
|
7631
|
-
process.on("SIGINT", shutdown);
|
|
7632
|
-
process.on("SIGTERM", shutdown);
|
|
7633
|
-
await new Promise(() => {
|
|
7634
|
-
});
|
|
7887
|
+
fs36.closeSync(out);
|
|
7888
|
+
const url = await waitForUrlFile(webUrlFilePath(), 5e3);
|
|
7889
|
+
return url;
|
|
7635
7890
|
}
|
|
7636
7891
|
async function tryRegisterWithWorkWeb(ctx, routeKind) {
|
|
7637
|
-
const
|
|
7638
|
-
|
|
7639
|
-
try {
|
|
7640
|
-
webUrl = fs32.readFileSync(webUrlFile, "utf-8").trim();
|
|
7641
|
-
if (!webUrl) return null;
|
|
7642
|
-
} catch {
|
|
7643
|
-
return null;
|
|
7644
|
-
}
|
|
7892
|
+
const webUrl = await ensureWorkWebRunning();
|
|
7893
|
+
if (!webUrl) return null;
|
|
7645
7894
|
try {
|
|
7646
7895
|
const res = await fetch(`${webUrl}api/scopes`, {
|
|
7647
7896
|
method: "POST",
|
|
@@ -7662,49 +7911,27 @@ async function tryRegisterWithWorkWeb(ctx, routeKind) {
|
|
|
7662
7911
|
async function runLauncher(ctx) {
|
|
7663
7912
|
const webRouteUrl = await tryRegisterWithWorkWeb(ctx, "diff");
|
|
7664
7913
|
if (webRouteUrl) {
|
|
7665
|
-
info(chalk21.gray(`Opening
|
|
7914
|
+
info(chalk21.gray(`Opening: ${webRouteUrl}`));
|
|
7666
7915
|
openUrl(webRouteUrl);
|
|
7667
7916
|
return;
|
|
7668
7917
|
}
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
try {
|
|
7679
|
-
fs32.unlinkSync(ctx.paths.pid);
|
|
7680
|
-
} catch {
|
|
7681
|
-
}
|
|
7682
|
-
try {
|
|
7683
|
-
fs32.unlinkSync(ctx.paths.url);
|
|
7684
|
-
} catch {
|
|
7685
|
-
}
|
|
7686
|
-
const passthrough = process.argv.slice(2).filter((a) => a !== "--stop" && a !== "--watch");
|
|
7687
|
-
const pid = spawnDaemon(passthrough, ctx.paths.log);
|
|
7688
|
-
info(chalk21.gray(`Started watcher (PID ${pid}). Log: ${ctx.paths.log}`));
|
|
7689
|
-
info(chalk21.gray("Stop with: wd --stop"));
|
|
7690
|
-
url = await waitForUrlFile(ctx.paths.url, 3e3);
|
|
7691
|
-
}
|
|
7692
|
-
if (!url) {
|
|
7693
|
-
console.error(
|
|
7694
|
-
chalk21.red("Watcher did not report a URL \u2014 check the log:"),
|
|
7695
|
-
ctx.paths.log
|
|
7696
|
-
);
|
|
7697
|
-
return;
|
|
7698
|
-
}
|
|
7699
|
-
info(chalk21.gray(`URL: ${url}`));
|
|
7700
|
-
openUrl(url);
|
|
7918
|
+
console.error(
|
|
7919
|
+
chalk21.red("Could not start or reach work web.")
|
|
7920
|
+
);
|
|
7921
|
+
console.error(
|
|
7922
|
+
chalk21.gray(
|
|
7923
|
+
`Tail ~/.work/web-autostart.log for diagnostics, or run \`work web\` in another shell to inspect startup directly.`
|
|
7924
|
+
)
|
|
7925
|
+
);
|
|
7926
|
+
process.exitCode = 1;
|
|
7701
7927
|
}
|
|
7702
7928
|
function runStatic(ctx, initialBranch) {
|
|
7703
7929
|
const uncommitted = buildRepoSpecs(ctx.scope, "HEAD");
|
|
7704
7930
|
const primaryRoot = ctx.scope.repos.find((r) => r.name === ctx.scope.activeRepoName)?.root ?? ctx.scope.repos[0].root;
|
|
7705
|
-
const
|
|
7931
|
+
const perRepoBase = ctx.scope.session?.baseBranches;
|
|
7932
|
+
const parent = perRepoBase?.[primaryRoot] ?? ctx.scope.session?.baseBranch ?? findAnyParentBranch(primaryRoot);
|
|
7706
7933
|
const branch = parent === null ? void 0 : {
|
|
7707
|
-
specs: buildRepoSpecs(ctx.scope, parent),
|
|
7934
|
+
specs: buildRepoSpecs(ctx.scope, parent, perRepoBase),
|
|
7708
7935
|
resolvedBase: parent
|
|
7709
7936
|
};
|
|
7710
7937
|
const uncommittedTotal = uncommitted.reduce(
|
|
@@ -7725,8 +7952,8 @@ function runStatic(ctx, initialBranch) {
|
|
|
7725
7952
|
branch,
|
|
7726
7953
|
initialBase: initialBranch && branch ? "branch" : "uncommitted"
|
|
7727
7954
|
});
|
|
7728
|
-
const filePath = `${ctx.
|
|
7729
|
-
|
|
7955
|
+
const filePath = `${ctx.scopeStem}.html`;
|
|
7956
|
+
fs36.writeFileSync(filePath, html, "utf-8");
|
|
7730
7957
|
info(chalk21.gray(`Wrote ${filePath}`));
|
|
7731
7958
|
openUrl(`file:///${filePath.replace(/\\/g, "/")}`);
|
|
7732
7959
|
}
|
|
@@ -7735,7 +7962,7 @@ function waitForUrlFile(filePath, timeoutMs) {
|
|
|
7735
7962
|
const start = Date.now();
|
|
7736
7963
|
const tick = () => {
|
|
7737
7964
|
try {
|
|
7738
|
-
const v =
|
|
7965
|
+
const v = fs36.readFileSync(filePath, "utf-8").trim();
|
|
7739
7966
|
if (v) return resolve(v);
|
|
7740
7967
|
} catch {
|
|
7741
7968
|
}
|
|
@@ -7746,14 +7973,8 @@ function waitForUrlFile(filePath, timeoutMs) {
|
|
|
7746
7973
|
});
|
|
7747
7974
|
}
|
|
7748
7975
|
async function tryReviewViaWorkWeb(ctx) {
|
|
7749
|
-
const
|
|
7750
|
-
|
|
7751
|
-
try {
|
|
7752
|
-
webUrl = fs32.readFileSync(webUrlFile, "utf-8").trim();
|
|
7753
|
-
if (!webUrl) return false;
|
|
7754
|
-
} catch {
|
|
7755
|
-
return false;
|
|
7756
|
-
}
|
|
7976
|
+
const webUrl = await ensureWorkWebRunning();
|
|
7977
|
+
if (!webUrl) return false;
|
|
7757
7978
|
let hash;
|
|
7758
7979
|
try {
|
|
7759
7980
|
const res = await fetch(`${webUrl}api/scopes`, {
|
|
@@ -7881,6 +8102,7 @@ summary-id: ${info3.summary.id}` : ""}
|
|
|
7881
8102
|
repos: ctx.repoSpecs,
|
|
7882
8103
|
scopeLabel: ctx.scopeLabel,
|
|
7883
8104
|
sessionBaseBranch: ctx.scope.session?.baseBranch,
|
|
8105
|
+
sessionBaseBranches: ctx.scope.session?.baseBranches,
|
|
7884
8106
|
onComment,
|
|
7885
8107
|
onCommentDeleted,
|
|
7886
8108
|
onSubmitReviewStart,
|
|
@@ -7942,12 +8164,7 @@ var diffCommand = {
|
|
|
7942
8164
|
}).option("stop", {
|
|
7943
8165
|
type: "boolean",
|
|
7944
8166
|
default: false,
|
|
7945
|
-
describe: "
|
|
7946
|
-
}).option("watch-daemon", {
|
|
7947
|
-
type: "boolean",
|
|
7948
|
-
default: false,
|
|
7949
|
-
hidden: true,
|
|
7950
|
-
describe: "Internal: run the foreground watcher loop."
|
|
8167
|
+
describe: "De-register this scope from work web. The work web server itself keeps running; use `work web --stop` to terminate the server."
|
|
7951
8168
|
}).option("comments", {
|
|
7952
8169
|
type: "boolean",
|
|
7953
8170
|
alias: "c",
|
|
@@ -7965,7 +8182,8 @@ var diffCommand = {
|
|
|
7965
8182
|
branch: argv.branch
|
|
7966
8183
|
});
|
|
7967
8184
|
const repoSpecs = buildRepoSpecs(scope, base);
|
|
7968
|
-
const
|
|
8185
|
+
const scopeStem = scopePathStem(repoSpecs);
|
|
8186
|
+
if (argv.stop) return runStop(repoSpecs);
|
|
7969
8187
|
if (base === "HEAD") {
|
|
7970
8188
|
info(chalk21.gray("Showing uncommitted changes vs HEAD."));
|
|
7971
8189
|
} else {
|
|
@@ -7981,11 +8199,9 @@ var diffCommand = {
|
|
|
7981
8199
|
base,
|
|
7982
8200
|
baseSource,
|
|
7983
8201
|
repoSpecs,
|
|
7984
|
-
|
|
8202
|
+
scopeStem,
|
|
7985
8203
|
scopeLabel
|
|
7986
8204
|
};
|
|
7987
|
-
if (argv.stop) return runStop(ctx.paths);
|
|
7988
|
-
if (argv["watch-daemon"]) return runDaemon(ctx);
|
|
7989
8205
|
if (argv.comments) return runReview(ctx);
|
|
7990
8206
|
if (argv.static) return runStatic(ctx, !!argv.branch);
|
|
7991
8207
|
return runLauncher(ctx);
|
|
@@ -7993,21 +8209,21 @@ var diffCommand = {
|
|
|
7993
8209
|
};
|
|
7994
8210
|
|
|
7995
8211
|
// src/commands/web.ts
|
|
7996
|
-
import
|
|
8212
|
+
import fs41 from "fs";
|
|
7997
8213
|
import os13 from "os";
|
|
7998
|
-
import
|
|
8214
|
+
import path42 from "path";
|
|
7999
8215
|
import chalk23 from "chalk";
|
|
8000
8216
|
|
|
8001
8217
|
// src/core/web-server.ts
|
|
8002
|
-
import
|
|
8218
|
+
import fs40 from "fs";
|
|
8003
8219
|
import os12 from "os";
|
|
8004
|
-
import
|
|
8220
|
+
import path41 from "path";
|
|
8005
8221
|
import chalk22 from "chalk";
|
|
8006
8222
|
import { Hono as Hono3 } from "hono";
|
|
8007
8223
|
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
8008
8224
|
|
|
8009
8225
|
// src/core/web-state.ts
|
|
8010
|
-
import
|
|
8226
|
+
import path34 from "path";
|
|
8011
8227
|
import crypto4 from "crypto";
|
|
8012
8228
|
import chokidar2 from "chokidar";
|
|
8013
8229
|
function sessionIdFor(s) {
|
|
@@ -8028,7 +8244,7 @@ function subscribeSession(sessionId, onChange) {
|
|
|
8028
8244
|
const watcher = chokidar2.watch(roots, {
|
|
8029
8245
|
ignored: (filePath) => {
|
|
8030
8246
|
for (const r of roots) {
|
|
8031
|
-
const rel =
|
|
8247
|
+
const rel = path34.relative(r, filePath).replace(/\\/g, "/");
|
|
8032
8248
|
if (rel === ".git" || rel.startsWith(".git/")) return true;
|
|
8033
8249
|
}
|
|
8034
8250
|
return false;
|
|
@@ -8142,24 +8358,24 @@ function disposeAllPtys() {
|
|
|
8142
8358
|
}
|
|
8143
8359
|
|
|
8144
8360
|
// src/core/comment-file-store.ts
|
|
8145
|
-
import
|
|
8146
|
-
import
|
|
8361
|
+
import fs37 from "fs";
|
|
8362
|
+
import path35 from "path";
|
|
8147
8363
|
import os10 from "os";
|
|
8148
8364
|
function commentsDir() {
|
|
8149
|
-
return
|
|
8365
|
+
return path35.join(os10.homedir(), ".work", "comments");
|
|
8150
8366
|
}
|
|
8151
8367
|
function ensureDir() {
|
|
8152
|
-
|
|
8368
|
+
fs37.mkdirSync(commentsDir(), { recursive: true });
|
|
8153
8369
|
}
|
|
8154
8370
|
function commentsFileFor(sessionId) {
|
|
8155
|
-
return
|
|
8371
|
+
return path35.join(commentsDir(), `${sessionId}.json`);
|
|
8156
8372
|
}
|
|
8157
8373
|
function pathFor(sessionId) {
|
|
8158
8374
|
return commentsFileFor(sessionId);
|
|
8159
8375
|
}
|
|
8160
8376
|
function readDisk(sessionId) {
|
|
8161
8377
|
try {
|
|
8162
|
-
const raw =
|
|
8378
|
+
const raw = fs37.readFileSync(pathFor(sessionId), "utf-8");
|
|
8163
8379
|
const parsed = JSON.parse(raw);
|
|
8164
8380
|
return Array.isArray(parsed) ? parsed : [];
|
|
8165
8381
|
} catch {
|
|
@@ -8231,29 +8447,29 @@ function clearCommentStoreCache() {
|
|
|
8231
8447
|
}
|
|
8232
8448
|
|
|
8233
8449
|
// src/core/pending-delivery.ts
|
|
8234
|
-
import
|
|
8235
|
-
import
|
|
8450
|
+
import fs38 from "fs";
|
|
8451
|
+
import path36 from "path";
|
|
8236
8452
|
function pathFor2(sessionId) {
|
|
8237
8453
|
return {
|
|
8238
8454
|
comments: commentsFileFor(sessionId),
|
|
8239
|
-
delivered:
|
|
8455
|
+
delivered: path36.join(commentsDir(), `${sessionId}.delivered.json`)
|
|
8240
8456
|
};
|
|
8241
8457
|
}
|
|
8242
8458
|
function readJson(filePath, fallback) {
|
|
8243
8459
|
try {
|
|
8244
|
-
return JSON.parse(
|
|
8460
|
+
return JSON.parse(fs38.readFileSync(filePath, "utf8"));
|
|
8245
8461
|
} catch {
|
|
8246
8462
|
return fallback;
|
|
8247
8463
|
}
|
|
8248
8464
|
}
|
|
8249
8465
|
function writeAtomic2(filePath, content) {
|
|
8250
|
-
|
|
8466
|
+
fs38.mkdirSync(path36.dirname(filePath), { recursive: true });
|
|
8251
8467
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
8252
|
-
|
|
8253
|
-
|
|
8468
|
+
fs38.writeFileSync(tmp, content, "utf-8");
|
|
8469
|
+
fs38.renameSync(tmp, filePath);
|
|
8254
8470
|
}
|
|
8255
8471
|
function normalize(p) {
|
|
8256
|
-
return
|
|
8472
|
+
return path36.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
8257
8473
|
}
|
|
8258
8474
|
function findSessionForCwd(cwd) {
|
|
8259
8475
|
const norm = normalize(cwd);
|
|
@@ -8274,13 +8490,40 @@ function findSessionForCwd(cwd) {
|
|
|
8274
8490
|
}
|
|
8275
8491
|
return best?.session ?? null;
|
|
8276
8492
|
}
|
|
8493
|
+
function isPendingFor(delivered) {
|
|
8494
|
+
return (c) => c.status === "published" && c.author === "user" && !delivered.has(c.id);
|
|
8495
|
+
}
|
|
8277
8496
|
function readPendingForSession(sessionId) {
|
|
8278
8497
|
const paths = pathFor2(sessionId);
|
|
8279
8498
|
const comments = getCommentFileStore(sessionId).snapshot();
|
|
8280
8499
|
const delivered = new Set(readJson(paths.delivered, []));
|
|
8281
|
-
return comments.filter(
|
|
8282
|
-
|
|
8500
|
+
return comments.filter(isPendingFor(delivered));
|
|
8501
|
+
}
|
|
8502
|
+
function scopeStoreIdsForPaths(paths) {
|
|
8503
|
+
const resolved = paths.map((p) => path36.resolve(p));
|
|
8504
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8505
|
+
ids.add(`scope-${scopeHashFor(resolved)}`);
|
|
8506
|
+
for (const p of resolved) ids.add(`scope-${scopeHashFor([p])}`);
|
|
8507
|
+
return [...ids];
|
|
8508
|
+
}
|
|
8509
|
+
function readPendingForWorktree(session) {
|
|
8510
|
+
const sessionId = sessionIdFor(session);
|
|
8511
|
+
const delivered = new Set(
|
|
8512
|
+
readJson(pathFor2(sessionId).delivered, [])
|
|
8283
8513
|
);
|
|
8514
|
+
const pending = isPendingFor(delivered);
|
|
8515
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8516
|
+
const out = [];
|
|
8517
|
+
const collect = (storeId) => {
|
|
8518
|
+
for (const c of getCommentFileStore(storeId).snapshot()) {
|
|
8519
|
+
if (seen.has(c.id) || !pending(c)) continue;
|
|
8520
|
+
seen.add(c.id);
|
|
8521
|
+
out.push(c);
|
|
8522
|
+
}
|
|
8523
|
+
};
|
|
8524
|
+
collect(sessionId);
|
|
8525
|
+
for (const storeId of scopeStoreIdsForPaths(session.paths)) collect(storeId);
|
|
8526
|
+
return out;
|
|
8284
8527
|
}
|
|
8285
8528
|
function markDelivered(sessionId, ids) {
|
|
8286
8529
|
if (ids.length === 0) return;
|
|
@@ -8580,8 +8823,8 @@ function mountPanesRoutes(app, opts) {
|
|
|
8580
8823
|
}
|
|
8581
8824
|
|
|
8582
8825
|
// src/core/worktree-routes.ts
|
|
8583
|
-
import
|
|
8584
|
-
import { spawn as
|
|
8826
|
+
import path37 from "path";
|
|
8827
|
+
import { spawn as spawn11 } from "child_process";
|
|
8585
8828
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
8586
8829
|
import { z as z3 } from "zod";
|
|
8587
8830
|
function mountWorktreeRoutes(app, opts) {
|
|
@@ -8704,10 +8947,10 @@ function mountWorktreeRoutes(app, opts) {
|
|
|
8704
8947
|
const id = c.req.param("id");
|
|
8705
8948
|
const session = findSession2(id);
|
|
8706
8949
|
if (!session) return c.json({ error: "unknown session" }, 404);
|
|
8707
|
-
const target = session.isGroup ?
|
|
8950
|
+
const target = session.isGroup ? path37.dirname(session.paths[0]) : session.paths[0];
|
|
8708
8951
|
try {
|
|
8709
8952
|
const cmd = process.platform === "win32" ? "code.cmd" : "code";
|
|
8710
|
-
const child =
|
|
8953
|
+
const child = spawn11(cmd, [target], {
|
|
8711
8954
|
detached: true,
|
|
8712
8955
|
stdio: "ignore",
|
|
8713
8956
|
shell: false
|
|
@@ -8724,28 +8967,27 @@ function mountWorktreeRoutes(app, opts) {
|
|
|
8724
8967
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
8725
8968
|
import { z as z4 } from "zod";
|
|
8726
8969
|
import { EventEmitter } from "events";
|
|
8727
|
-
import
|
|
8728
|
-
import
|
|
8970
|
+
import path40 from "path";
|
|
8971
|
+
import spawn13 from "cross-spawn";
|
|
8729
8972
|
|
|
8730
8973
|
// src/core/checkpoint.ts
|
|
8731
|
-
import
|
|
8974
|
+
import fs39 from "fs";
|
|
8732
8975
|
import os11 from "os";
|
|
8733
|
-
import
|
|
8734
|
-
import
|
|
8735
|
-
import spawn10 from "cross-spawn";
|
|
8976
|
+
import path38 from "path";
|
|
8977
|
+
import spawn12 from "cross-spawn";
|
|
8736
8978
|
function manifestPath(scopeHash) {
|
|
8737
|
-
const dir =
|
|
8738
|
-
|
|
8739
|
-
return
|
|
8979
|
+
const dir = path38.join(os11.homedir(), ".work", "diffs");
|
|
8980
|
+
fs39.mkdirSync(dir, { recursive: true });
|
|
8981
|
+
return path38.join(dir, `${scopeHash}.checkpoints.json`);
|
|
8740
8982
|
}
|
|
8741
8983
|
function emptyManifest(scopeHash) {
|
|
8742
8984
|
return { version: 1, scopeHash, entries: [] };
|
|
8743
8985
|
}
|
|
8744
8986
|
function loadManifest(scopeHash) {
|
|
8745
8987
|
const file = manifestPath(scopeHash);
|
|
8746
|
-
if (!
|
|
8988
|
+
if (!fs39.existsSync(file)) return emptyManifest(scopeHash);
|
|
8747
8989
|
try {
|
|
8748
|
-
const raw =
|
|
8990
|
+
const raw = fs39.readFileSync(file, "utf-8");
|
|
8749
8991
|
const parsed = JSON.parse(raw);
|
|
8750
8992
|
if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
8751
8993
|
return emptyManifest(scopeHash);
|
|
@@ -8759,68 +9001,37 @@ function loadManifest(scopeHash) {
|
|
|
8759
9001
|
return emptyManifest(scopeHash);
|
|
8760
9002
|
}
|
|
8761
9003
|
}
|
|
8762
|
-
function snapshotRepo(repoRoot, scopeHash, id) {
|
|
8763
|
-
const
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
const
|
|
9004
|
+
function snapshotRepo(repoRoot, scopeHash, id, includeWorkingTree = true) {
|
|
9005
|
+
const tree = writeTempTree(repoRoot, { includeWorkingTree });
|
|
9006
|
+
if (!tree) return null;
|
|
9007
|
+
const { treeSha, headSha } = tree;
|
|
9008
|
+
const commitArgs = ["commit-tree", treeSha, "-m", "wd checkpoint"];
|
|
9009
|
+
if (headSha) commitArgs.push("-p", headSha);
|
|
9010
|
+
const commitEnv = {
|
|
9011
|
+
...process.env,
|
|
9012
|
+
GIT_AUTHOR_NAME: "wd",
|
|
9013
|
+
GIT_AUTHOR_EMAIL: "wd@local",
|
|
9014
|
+
GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
|
|
9015
|
+
GIT_COMMITTER_NAME: "wd",
|
|
9016
|
+
GIT_COMMITTER_EMAIL: "wd@local",
|
|
9017
|
+
GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
|
|
9018
|
+
};
|
|
9019
|
+
const commit = spawn12.sync("git", commitArgs, {
|
|
8769
9020
|
cwd: repoRoot,
|
|
8770
9021
|
encoding: "utf-8",
|
|
8771
|
-
env,
|
|
8772
|
-
windowsHide: true
|
|
8773
|
-
maxBuffer: 64 * 1024 * 1024
|
|
9022
|
+
env: commitEnv,
|
|
9023
|
+
windowsHide: true
|
|
8774
9024
|
});
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
}
|
|
8786
|
-
const add = run2(["add", "-A"]);
|
|
8787
|
-
if (add.status !== 0) return null;
|
|
8788
|
-
const wt = run2(["write-tree"]);
|
|
8789
|
-
if (wt.status !== 0 || !wt.stdout) return null;
|
|
8790
|
-
const treeSha = wt.stdout.trim();
|
|
8791
|
-
const commitArgs = ["commit-tree", treeSha, "-m", "wd checkpoint"];
|
|
8792
|
-
if (hasHead) commitArgs.push("-p", headSha);
|
|
8793
|
-
const commitEnv = {
|
|
8794
|
-
...process.env,
|
|
8795
|
-
GIT_AUTHOR_NAME: "wd",
|
|
8796
|
-
GIT_AUTHOR_EMAIL: "wd@local",
|
|
8797
|
-
GIT_AUTHOR_DATE: "2000-01-01T00:00:00Z",
|
|
8798
|
-
GIT_COMMITTER_NAME: "wd",
|
|
8799
|
-
GIT_COMMITTER_EMAIL: "wd@local",
|
|
8800
|
-
GIT_COMMITTER_DATE: "2000-01-01T00:00:00Z"
|
|
8801
|
-
};
|
|
8802
|
-
const commit = spawn10.sync("git", commitArgs, {
|
|
8803
|
-
cwd: repoRoot,
|
|
8804
|
-
encoding: "utf-8",
|
|
8805
|
-
env: commitEnv,
|
|
8806
|
-
windowsHide: true
|
|
8807
|
-
});
|
|
8808
|
-
if (commit.status !== 0 || !commit.stdout) return null;
|
|
8809
|
-
const commitSha = commit.stdout.trim();
|
|
8810
|
-
const refName = `refs/wd/${scopeHash}/${id}`;
|
|
8811
|
-
const updateRef = spawn10.sync(
|
|
8812
|
-
"git",
|
|
8813
|
-
["update-ref", refName, commitSha],
|
|
8814
|
-
{ cwd: repoRoot, encoding: "utf-8", windowsHide: true }
|
|
8815
|
-
);
|
|
8816
|
-
if (updateRef.status !== 0) return null;
|
|
8817
|
-
return commitSha;
|
|
8818
|
-
} finally {
|
|
8819
|
-
try {
|
|
8820
|
-
if (fs35.existsSync(tmpIndex)) fs35.unlinkSync(tmpIndex);
|
|
8821
|
-
} catch {
|
|
8822
|
-
}
|
|
8823
|
-
}
|
|
9025
|
+
if (commit.status !== 0 || !commit.stdout) return null;
|
|
9026
|
+
const commitSha = commit.stdout.trim();
|
|
9027
|
+
const refName = `refs/wd/${scopeHash}/${id}`;
|
|
9028
|
+
const updateRef = spawn12.sync("git", ["update-ref", refName, commitSha], {
|
|
9029
|
+
cwd: repoRoot,
|
|
9030
|
+
encoding: "utf-8",
|
|
9031
|
+
windowsHide: true
|
|
9032
|
+
});
|
|
9033
|
+
if (updateRef.status !== 0) return null;
|
|
9034
|
+
return commitSha;
|
|
8824
9035
|
}
|
|
8825
9036
|
async function takeCheckpoint(scopeHash, repos, opts = {}) {
|
|
8826
9037
|
const file = manifestPath(scopeHash);
|
|
@@ -8831,12 +9042,17 @@ async function takeCheckpoint(scopeHash, repos, opts = {}) {
|
|
|
8831
9042
|
const nextId = isFirst ? 0 : manifest.entries[manifest.entries.length - 1].id + 1;
|
|
8832
9043
|
const captured = {};
|
|
8833
9044
|
for (const repo of repos) {
|
|
8834
|
-
captured[repo.name] = snapshotRepo(
|
|
9045
|
+
captured[repo.name] = snapshotRepo(
|
|
9046
|
+
repo.root,
|
|
9047
|
+
scopeHash,
|
|
9048
|
+
nextId,
|
|
9049
|
+
!isFirst
|
|
9050
|
+
);
|
|
8835
9051
|
}
|
|
8836
9052
|
const rollbackRefs = () => {
|
|
8837
9053
|
const refName = `refs/wd/${scopeHash}/${nextId}`;
|
|
8838
9054
|
for (const repo of repos) {
|
|
8839
|
-
|
|
9055
|
+
spawn12.sync("git", ["update-ref", "-d", refName], {
|
|
8840
9056
|
cwd: repo.root,
|
|
8841
9057
|
encoding: "utf-8",
|
|
8842
9058
|
windowsHide: true
|
|
@@ -8849,10 +9065,22 @@ async function takeCheckpoint(scopeHash, repos, opts = {}) {
|
|
|
8849
9065
|
}
|
|
8850
9066
|
if (!opts.force && !isFirst) {
|
|
8851
9067
|
const prev = manifest.entries[manifest.entries.length - 1];
|
|
8852
|
-
const
|
|
8853
|
-
(
|
|
8854
|
-
|
|
8855
|
-
|
|
9068
|
+
const treeOf = (root, commitSha) => {
|
|
9069
|
+
if (!commitSha) return null;
|
|
9070
|
+
const r = spawn12.sync(
|
|
9071
|
+
"git",
|
|
9072
|
+
["rev-parse", `${commitSha}^{tree}`],
|
|
9073
|
+
{ cwd: root, encoding: "utf-8", windowsHide: true }
|
|
9074
|
+
);
|
|
9075
|
+
if (r.status !== 0 || typeof r.stdout !== "string") return null;
|
|
9076
|
+
return r.stdout.trim() || null;
|
|
9077
|
+
};
|
|
9078
|
+
const allTreesMatch = repos.every((repo) => {
|
|
9079
|
+
const curTree = treeOf(repo.root, captured[repo.name]);
|
|
9080
|
+
const prevTree = treeOf(repo.root, prev.repos[repo.name] ?? null);
|
|
9081
|
+
return curTree !== null && curTree === prevTree;
|
|
9082
|
+
});
|
|
9083
|
+
if (allTreesMatch) {
|
|
8856
9084
|
rollbackRefs();
|
|
8857
9085
|
return null;
|
|
8858
9086
|
}
|
|
@@ -8874,7 +9102,7 @@ function clearCheckpoints(scopeHash, repoRoots) {
|
|
|
8874
9102
|
for (const root of repoRoots) {
|
|
8875
9103
|
for (const entry of manifest.entries) {
|
|
8876
9104
|
const refName = `refs/wd/${scopeHash}/${entry.id}`;
|
|
8877
|
-
|
|
9105
|
+
spawn12.sync("git", ["update-ref", "-d", refName], {
|
|
8878
9106
|
cwd: root,
|
|
8879
9107
|
encoding: "utf-8",
|
|
8880
9108
|
windowsHide: true
|
|
@@ -8882,21 +9110,21 @@ function clearCheckpoints(scopeHash, repoRoots) {
|
|
|
8882
9110
|
}
|
|
8883
9111
|
}
|
|
8884
9112
|
const file = manifestPath(scopeHash);
|
|
8885
|
-
if (
|
|
9113
|
+
if (fs39.existsSync(file)) {
|
|
8886
9114
|
try {
|
|
8887
|
-
|
|
9115
|
+
fs39.unlinkSync(file);
|
|
8888
9116
|
} catch {
|
|
8889
9117
|
}
|
|
8890
9118
|
}
|
|
8891
9119
|
}
|
|
8892
9120
|
|
|
8893
9121
|
// src/core/scope-manager.ts
|
|
8894
|
-
import
|
|
8895
|
-
import
|
|
9122
|
+
import path39 from "path";
|
|
9123
|
+
import crypto5 from "crypto";
|
|
8896
9124
|
var scopes = /* @__PURE__ */ new Map();
|
|
8897
9125
|
function hashFor(paths) {
|
|
8898
9126
|
const key = paths.slice().sort().join("|");
|
|
8899
|
-
return
|
|
9127
|
+
return crypto5.createHash("sha1").update(key).digest("hex").slice(0, 12);
|
|
8900
9128
|
}
|
|
8901
9129
|
var ScopePathRejectedError = class extends Error {
|
|
8902
9130
|
constructor(rejected) {
|
|
@@ -8908,7 +9136,7 @@ var ScopePathRejectedError = class extends Error {
|
|
|
8908
9136
|
}
|
|
8909
9137
|
};
|
|
8910
9138
|
function normaliseForCompare(p) {
|
|
8911
|
-
return
|
|
9139
|
+
return path39.resolve(p).replace(/\\/g, "/").toLowerCase();
|
|
8912
9140
|
}
|
|
8913
9141
|
function rejectedPaths(normalised) {
|
|
8914
9142
|
const config = loadConfig();
|
|
@@ -8926,7 +9154,7 @@ function rejectedPaths(normalised) {
|
|
|
8926
9154
|
});
|
|
8927
9155
|
}
|
|
8928
9156
|
function registerScope(paths, label2) {
|
|
8929
|
-
const normalised = paths.map((p) =>
|
|
9157
|
+
const normalised = paths.map((p) => path39.resolve(p));
|
|
8930
9158
|
const rejected = rejectedPaths(normalised);
|
|
8931
9159
|
if (rejected.length > 0) throw new ScopePathRejectedError(rejected);
|
|
8932
9160
|
const hash = hashFor(normalised);
|
|
@@ -8940,7 +9168,7 @@ function registerScope(paths, label2) {
|
|
|
8940
9168
|
const scope = {
|
|
8941
9169
|
hash,
|
|
8942
9170
|
paths: normalised,
|
|
8943
|
-
label: label2 ??
|
|
9171
|
+
label: label2 ?? path39.basename(normalised[0]),
|
|
8944
9172
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8945
9173
|
ended: false
|
|
8946
9174
|
};
|
|
@@ -9013,7 +9241,7 @@ function mountScopeRoutes(app, opts) {
|
|
|
9013
9241
|
function workingTreeFingerprint(paths) {
|
|
9014
9242
|
const parts = [];
|
|
9015
9243
|
for (const p of paths) {
|
|
9016
|
-
const r =
|
|
9244
|
+
const r = spawn13.sync(
|
|
9017
9245
|
"git",
|
|
9018
9246
|
["status", "--porcelain", "--no-renames", "-z"],
|
|
9019
9247
|
{ cwd: p, encoding: "utf-8", windowsHide: true }
|
|
@@ -9137,7 +9365,7 @@ function mountScopeRoutes(app, opts) {
|
|
|
9137
9365
|
const fromSha = fromEntry.repos[p] ?? "HEAD";
|
|
9138
9366
|
const toSha = toEntry === void 0 ? "working" : toEntry.repos[p] ?? "HEAD";
|
|
9139
9367
|
return {
|
|
9140
|
-
name:
|
|
9368
|
+
name: path40.basename(p),
|
|
9141
9369
|
root: p,
|
|
9142
9370
|
files: computeRangeDiff({ root: p, fromRef: fromSha, toRef: toSha })
|
|
9143
9371
|
};
|
|
@@ -9151,19 +9379,46 @@ function mountScopeRoutes(app, opts) {
|
|
|
9151
9379
|
repos: repos2
|
|
9152
9380
|
});
|
|
9153
9381
|
}
|
|
9154
|
-
const resolved = scope.paths.map(
|
|
9382
|
+
const resolved = scope.paths.map(
|
|
9383
|
+
(p) => resolveRepoDiff(p, base, sessionBaseForPath(p))
|
|
9384
|
+
);
|
|
9155
9385
|
const repos = scope.paths.map((p, i) => ({
|
|
9156
|
-
name:
|
|
9386
|
+
name: path40.basename(p),
|
|
9157
9387
|
root: p,
|
|
9158
9388
|
resolvedBase: resolved[i].resolvedBase,
|
|
9159
9389
|
files: computeDiff({ root: p, diffArg: resolved[i].diffArg })
|
|
9160
9390
|
}));
|
|
9161
9391
|
const resolvedBase = resolved[0]?.resolvedBase ?? "HEAD";
|
|
9162
|
-
|
|
9392
|
+
const head = git(["rev-parse", "--abbrev-ref", "HEAD"], scope.paths[0]);
|
|
9393
|
+
const headBranch = head.exitCode === 0 && head.stdout && head.stdout !== "HEAD" ? head.stdout : void 0;
|
|
9394
|
+
return c.json({
|
|
9395
|
+
scopeHash: scope.hash,
|
|
9396
|
+
base,
|
|
9397
|
+
resolvedBase,
|
|
9398
|
+
headBranch,
|
|
9399
|
+
repos
|
|
9400
|
+
});
|
|
9163
9401
|
} catch (err) {
|
|
9164
9402
|
return c.json({ error: err.message }, 500);
|
|
9165
9403
|
}
|
|
9166
9404
|
});
|
|
9405
|
+
app.get("/api/scopes/:hash/file-lines", (c) => {
|
|
9406
|
+
const scope = getScope(c.req.param("hash"));
|
|
9407
|
+
if (!scope) return c.json({ error: "unknown scope" }, 404);
|
|
9408
|
+
const repoName = c.req.query("repo") ?? "";
|
|
9409
|
+
const relPath = c.req.query("path") ?? "";
|
|
9410
|
+
const start = Number(c.req.query("start"));
|
|
9411
|
+
const end = Number(c.req.query("end"));
|
|
9412
|
+
const ref = c.req.query("ref") || void 0;
|
|
9413
|
+
if (!relPath || !Number.isInteger(start) || !Number.isInteger(end)) {
|
|
9414
|
+
return c.json({ error: "bad path/start/end" }, 400);
|
|
9415
|
+
}
|
|
9416
|
+
const root = scope.paths.length === 1 ? scope.paths[0] : scope.paths.find((p) => path40.basename(p) === repoName);
|
|
9417
|
+
if (!root) return c.json({ error: "unknown repo" }, 404);
|
|
9418
|
+
const result = readContextLines({ root, relPath, start, end, ref });
|
|
9419
|
+
if (!result) return c.json({ error: "cannot read file" }, 400);
|
|
9420
|
+
return c.json(result);
|
|
9421
|
+
});
|
|
9167
9422
|
app.get("/api/scopes/:hash/checkpoints", (c) => {
|
|
9168
9423
|
const scope = getScope(c.req.param("hash"));
|
|
9169
9424
|
if (!scope) return c.json({ error: "unknown scope" }, 404);
|
|
@@ -9392,16 +9647,19 @@ function sessionToWire(s) {
|
|
|
9392
9647
|
};
|
|
9393
9648
|
}
|
|
9394
9649
|
function computeSessionDiff(s, base) {
|
|
9395
|
-
const resolved = s.paths.map(
|
|
9650
|
+
const resolved = s.paths.map(
|
|
9651
|
+
(p) => resolveRepoDiff(p, base, s.baseBranches?.[p] ?? s.baseBranch)
|
|
9652
|
+
);
|
|
9396
9653
|
const repos = s.paths.map((p, i) => ({
|
|
9397
|
-
name:
|
|
9654
|
+
name: path41.basename(p),
|
|
9398
9655
|
root: p,
|
|
9399
9656
|
resolvedBase: resolved[i].resolvedBase,
|
|
9400
9657
|
files: computeDiff({ root: p, diffArg: resolved[i].diffArg })
|
|
9401
9658
|
}));
|
|
9402
9659
|
return { repos, resolvedBase: resolved[0]?.resolvedBase ?? "HEAD" };
|
|
9403
9660
|
}
|
|
9404
|
-
async function startWebServer() {
|
|
9661
|
+
async function startWebServer(opts = {}) {
|
|
9662
|
+
const { lean = false } = opts;
|
|
9405
9663
|
const webRoot = resolveWebRoot();
|
|
9406
9664
|
if (!webRoot) {
|
|
9407
9665
|
throw new Error(
|
|
@@ -9413,25 +9671,27 @@ async function startWebServer() {
|
|
|
9413
9671
|
for (const cb of sseListeners) cb({ event, data });
|
|
9414
9672
|
};
|
|
9415
9673
|
const home = os12.homedir();
|
|
9416
|
-
const historyPath =
|
|
9674
|
+
const historyPath = path41.join(home, ".work", "history.json");
|
|
9417
9675
|
const onHistoryChange = () => broadcast("sessions-changed", { ts: Date.now() });
|
|
9418
|
-
|
|
9419
|
-
const tasksPath =
|
|
9676
|
+
fs40.watchFile(historyPath, { interval: 1e3 }, onHistoryChange);
|
|
9677
|
+
const tasksPath = path41.join(home, ".work", "tasks.json");
|
|
9420
9678
|
const onTasksChange = () => broadcast("tasks-changed", { ts: Date.now() });
|
|
9421
|
-
|
|
9679
|
+
fs40.watchFile(tasksPath, { interval: 1e3 }, onTasksChange);
|
|
9422
9680
|
const projectsRoot = claudeProjectsRoot();
|
|
9423
9681
|
let activityWatcher = null;
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9682
|
+
if (!lean) {
|
|
9683
|
+
try {
|
|
9684
|
+
if (fs40.existsSync(projectsRoot)) {
|
|
9685
|
+
activityWatcher = createFsWatcher({
|
|
9686
|
+
roots: [projectsRoot],
|
|
9687
|
+
debounceMs: 250,
|
|
9688
|
+
onChange: () => broadcast("sessions-changed", { ts: Date.now() })
|
|
9689
|
+
});
|
|
9690
|
+
}
|
|
9691
|
+
} catch {
|
|
9431
9692
|
}
|
|
9432
|
-
} catch {
|
|
9433
9693
|
}
|
|
9434
|
-
const decayTick = setInterval(
|
|
9694
|
+
const decayTick = lean ? null : setInterval(
|
|
9435
9695
|
() => broadcast("sessions-changed", { ts: Date.now() }),
|
|
9436
9696
|
1e4
|
|
9437
9697
|
);
|
|
@@ -9496,9 +9756,9 @@ async function startWebServer() {
|
|
|
9496
9756
|
url: handle.url,
|
|
9497
9757
|
port: handle.port,
|
|
9498
9758
|
stop: async () => {
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
clearInterval(decayTick);
|
|
9759
|
+
fs40.unwatchFile(historyPath, onHistoryChange);
|
|
9760
|
+
fs40.unwatchFile(tasksPath, onTasksChange);
|
|
9761
|
+
if (decayTick) clearInterval(decayTick);
|
|
9502
9762
|
activityWatcher?.stop();
|
|
9503
9763
|
disposeAllWatchers();
|
|
9504
9764
|
for (const scope of listScopes()) {
|
|
@@ -9559,12 +9819,12 @@ function info2(message) {
|
|
|
9559
9819
|
process.stderr.write(message + "\n");
|
|
9560
9820
|
}
|
|
9561
9821
|
function urlFilePath() {
|
|
9562
|
-
return
|
|
9822
|
+
return path42.join(os13.homedir(), ".work", "web.url");
|
|
9563
9823
|
}
|
|
9564
9824
|
function pidFilePath() {
|
|
9565
|
-
return
|
|
9825
|
+
return path42.join(os13.homedir(), ".work", "web.pid");
|
|
9566
9826
|
}
|
|
9567
|
-
function
|
|
9827
|
+
function isPidAlive(pid) {
|
|
9568
9828
|
try {
|
|
9569
9829
|
process.kill(pid, 0);
|
|
9570
9830
|
return true;
|
|
@@ -9572,9 +9832,9 @@ function isPidAlive2(pid) {
|
|
|
9572
9832
|
return false;
|
|
9573
9833
|
}
|
|
9574
9834
|
}
|
|
9575
|
-
function
|
|
9835
|
+
function readPid() {
|
|
9576
9836
|
try {
|
|
9577
|
-
const raw =
|
|
9837
|
+
const raw = fs41.readFileSync(pidFilePath(), "utf-8").trim();
|
|
9578
9838
|
const n = Number(raw);
|
|
9579
9839
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
9580
9840
|
} catch {
|
|
@@ -9583,7 +9843,7 @@ function readPid2() {
|
|
|
9583
9843
|
}
|
|
9584
9844
|
function readUrl() {
|
|
9585
9845
|
try {
|
|
9586
|
-
const v =
|
|
9846
|
+
const v = fs41.readFileSync(urlFilePath(), "utf-8").trim();
|
|
9587
9847
|
return v || null;
|
|
9588
9848
|
} catch {
|
|
9589
9849
|
return null;
|
|
@@ -9601,19 +9861,19 @@ async function pingsAlive(url, timeoutMs = 500) {
|
|
|
9601
9861
|
}
|
|
9602
9862
|
}
|
|
9603
9863
|
function stopExisting() {
|
|
9604
|
-
const pid =
|
|
9864
|
+
const pid = readPid();
|
|
9605
9865
|
if (!pid) {
|
|
9606
9866
|
info2(chalk23.gray("No work web running."));
|
|
9607
9867
|
return false;
|
|
9608
9868
|
}
|
|
9609
|
-
if (!
|
|
9869
|
+
if (!isPidAlive(pid)) {
|
|
9610
9870
|
info2(chalk23.gray(`Stale PID ${pid} \u2014 cleaning up.`));
|
|
9611
9871
|
try {
|
|
9612
|
-
|
|
9872
|
+
fs41.unlinkSync(pidFilePath());
|
|
9613
9873
|
} catch {
|
|
9614
9874
|
}
|
|
9615
9875
|
try {
|
|
9616
|
-
|
|
9876
|
+
fs41.unlinkSync(urlFilePath());
|
|
9617
9877
|
} catch {
|
|
9618
9878
|
}
|
|
9619
9879
|
return false;
|
|
@@ -9622,11 +9882,11 @@ function stopExisting() {
|
|
|
9622
9882
|
process.kill(pid);
|
|
9623
9883
|
info2(chalk23.gray(`Stopped work web (PID ${pid}).`));
|
|
9624
9884
|
try {
|
|
9625
|
-
|
|
9885
|
+
fs41.unlinkSync(pidFilePath());
|
|
9626
9886
|
} catch {
|
|
9627
9887
|
}
|
|
9628
9888
|
try {
|
|
9629
|
-
|
|
9889
|
+
fs41.unlinkSync(urlFilePath());
|
|
9630
9890
|
} catch {
|
|
9631
9891
|
}
|
|
9632
9892
|
return true;
|
|
@@ -9646,14 +9906,19 @@ var webCommand = {
|
|
|
9646
9906
|
type: "boolean",
|
|
9647
9907
|
default: false,
|
|
9648
9908
|
describe: "Stop a running work web instance and exit."
|
|
9909
|
+
}).option("lean", {
|
|
9910
|
+
type: "boolean",
|
|
9911
|
+
default: false,
|
|
9912
|
+
hidden: true,
|
|
9913
|
+
describe: "Internal: start without dashboard-only features (Claude activity watcher + hooks). Used by `wd` when it auto-starts work web for a diff-only session."
|
|
9649
9914
|
}),
|
|
9650
9915
|
handler: async (argv) => {
|
|
9651
9916
|
if (argv.stop) {
|
|
9652
9917
|
stopExisting();
|
|
9653
9918
|
process.exit(0);
|
|
9654
9919
|
}
|
|
9655
|
-
const existingPid =
|
|
9656
|
-
if (existingPid &&
|
|
9920
|
+
const existingPid = readPid();
|
|
9921
|
+
if (existingPid && isPidAlive(existingPid)) {
|
|
9657
9922
|
const url = readUrl();
|
|
9658
9923
|
if (url && await pingsAlive(url)) {
|
|
9659
9924
|
info2(
|
|
@@ -9672,55 +9937,64 @@ var webCommand = {
|
|
|
9672
9937
|
process.exit(1);
|
|
9673
9938
|
}
|
|
9674
9939
|
try {
|
|
9675
|
-
|
|
9940
|
+
fs41.unlinkSync(pidFilePath());
|
|
9676
9941
|
} catch {
|
|
9677
9942
|
}
|
|
9678
9943
|
try {
|
|
9679
|
-
|
|
9944
|
+
fs41.unlinkSync(urlFilePath());
|
|
9680
9945
|
} catch {
|
|
9681
9946
|
}
|
|
9682
|
-
const
|
|
9947
|
+
const lean = !!argv.lean || process.env.WORK_WEB_LEAN === "1";
|
|
9948
|
+
const handle = await startWebServer({ lean });
|
|
9683
9949
|
try {
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9950
|
+
fs41.mkdirSync(path42.dirname(urlFilePath()), { recursive: true });
|
|
9951
|
+
fs41.writeFileSync(urlFilePath(), handle.url);
|
|
9952
|
+
fs41.writeFileSync(pidFilePath(), String(process.pid));
|
|
9687
9953
|
} catch {
|
|
9688
9954
|
}
|
|
9689
|
-
info2(
|
|
9955
|
+
info2(
|
|
9956
|
+
chalk23.gray(
|
|
9957
|
+
`work web running at ${handle.url}${lean ? " (lean \u2014 diff-only mode)" : ""}`
|
|
9958
|
+
)
|
|
9959
|
+
);
|
|
9690
9960
|
info2(chalk23.gray("Press Ctrl+C to stop. Or: `work web --stop` from another shell."));
|
|
9691
9961
|
if (argv.open) openUrl(handle.url);
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9962
|
+
if (!lean) {
|
|
9963
|
+
await Promise.all([
|
|
9964
|
+
installCommandHook({
|
|
9965
|
+
owner: "web",
|
|
9966
|
+
event: "UserPromptSubmit",
|
|
9967
|
+
command: "work hook prompt-submit",
|
|
9968
|
+
timeoutSec: 5
|
|
9969
|
+
}),
|
|
9970
|
+
installCommandHook({
|
|
9971
|
+
owner: "web",
|
|
9972
|
+
event: "Stop",
|
|
9973
|
+
command: "work hook stop",
|
|
9974
|
+
timeoutSec: 5
|
|
9975
|
+
})
|
|
9976
|
+
]).catch(() => {
|
|
9977
|
+
});
|
|
9978
|
+
}
|
|
9707
9979
|
const shutdown = () => {
|
|
9708
9980
|
info2(chalk23.gray("\nStopping work web."));
|
|
9709
9981
|
try {
|
|
9710
|
-
|
|
9982
|
+
fs41.unlinkSync(urlFilePath());
|
|
9711
9983
|
} catch {
|
|
9712
9984
|
}
|
|
9713
9985
|
try {
|
|
9714
|
-
|
|
9986
|
+
fs41.unlinkSync(pidFilePath());
|
|
9715
9987
|
} catch {
|
|
9716
9988
|
}
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9989
|
+
if (!lean) {
|
|
9990
|
+
try {
|
|
9991
|
+
removeCommandHookSync("web", "UserPromptSubmit");
|
|
9992
|
+
} catch {
|
|
9993
|
+
}
|
|
9994
|
+
try {
|
|
9995
|
+
removeCommandHookSync("web", "Stop");
|
|
9996
|
+
} catch {
|
|
9997
|
+
}
|
|
9724
9998
|
}
|
|
9725
9999
|
handle.stop();
|
|
9726
10000
|
process.exit(0);
|
|
@@ -9729,11 +10003,11 @@ var webCommand = {
|
|
|
9729
10003
|
process.on("SIGTERM", shutdown);
|
|
9730
10004
|
process.on("exit", () => {
|
|
9731
10005
|
try {
|
|
9732
|
-
|
|
10006
|
+
fs41.unlinkSync(pidFilePath());
|
|
9733
10007
|
} catch {
|
|
9734
10008
|
}
|
|
9735
10009
|
try {
|
|
9736
|
-
|
|
10010
|
+
fs41.unlinkSync(urlFilePath());
|
|
9737
10011
|
} catch {
|
|
9738
10012
|
}
|
|
9739
10013
|
});
|
|
@@ -9749,7 +10023,7 @@ function computeHookOutput(input2) {
|
|
|
9749
10023
|
const activity = readSessionActivity(session);
|
|
9750
10024
|
if (activity.state === "stale") return null;
|
|
9751
10025
|
const sessionId = sessionIdFor(session);
|
|
9752
|
-
const pending =
|
|
10026
|
+
const pending = readPendingForWorktree(session);
|
|
9753
10027
|
if (pending.length === 0) return null;
|
|
9754
10028
|
const text = formatPendingForPrompt(pending);
|
|
9755
10029
|
if (!text) return null;
|
|
@@ -9808,7 +10082,7 @@ var hookCommand = {
|
|
|
9808
10082
|
|
|
9809
10083
|
// src/commands/run.ts
|
|
9810
10084
|
import chalk24 from "chalk";
|
|
9811
|
-
import
|
|
10085
|
+
import spawn14 from "cross-spawn";
|
|
9812
10086
|
|
|
9813
10087
|
// src/core/fleet.ts
|
|
9814
10088
|
function selectSessions(sessions, filter) {
|
|
@@ -9850,7 +10124,7 @@ function killAllChildren(signal = "SIGTERM") {
|
|
|
9850
10124
|
function runInPath(unit, cmd, prefix) {
|
|
9851
10125
|
const { bin, args } = shellInvocation(cmd);
|
|
9852
10126
|
return new Promise((resolve) => {
|
|
9853
|
-
const child =
|
|
10127
|
+
const child = spawn14(bin, args, {
|
|
9854
10128
|
cwd: unit.path,
|
|
9855
10129
|
stdio: prefix ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
9856
10130
|
shell: false
|
|
@@ -10082,15 +10356,15 @@ var runCommand = {
|
|
|
10082
10356
|
};
|
|
10083
10357
|
|
|
10084
10358
|
// src/commands/broadcast.ts
|
|
10085
|
-
import
|
|
10359
|
+
import fs43 from "fs";
|
|
10086
10360
|
import chalk25 from "chalk";
|
|
10087
10361
|
|
|
10088
10362
|
// src/core/broadcast.ts
|
|
10089
|
-
import
|
|
10090
|
-
import
|
|
10363
|
+
import crypto6 from "crypto";
|
|
10364
|
+
import fs42 from "fs";
|
|
10091
10365
|
function readComments(file) {
|
|
10092
10366
|
try {
|
|
10093
|
-
const parsed = JSON.parse(
|
|
10367
|
+
const parsed = JSON.parse(fs42.readFileSync(file, "utf-8"));
|
|
10094
10368
|
return Array.isArray(parsed) ? parsed : [];
|
|
10095
10369
|
} catch {
|
|
10096
10370
|
return [];
|
|
@@ -10098,10 +10372,10 @@ function readComments(file) {
|
|
|
10098
10372
|
}
|
|
10099
10373
|
async function appendLocked(sessionId, body) {
|
|
10100
10374
|
const file = commentsFileFor(sessionId);
|
|
10101
|
-
|
|
10375
|
+
fs42.mkdirSync(commentsDir(), { recursive: true });
|
|
10102
10376
|
ensureFile(file, "[]");
|
|
10103
10377
|
const comment = {
|
|
10104
|
-
id:
|
|
10378
|
+
id: crypto6.randomBytes(8).toString("hex"),
|
|
10105
10379
|
repo: "",
|
|
10106
10380
|
file: "",
|
|
10107
10381
|
line: 0,
|
|
@@ -10135,7 +10409,7 @@ async function broadcastPrompt(sessions, filter, prompt) {
|
|
|
10135
10409
|
// src/commands/broadcast.ts
|
|
10136
10410
|
function readStdin() {
|
|
10137
10411
|
try {
|
|
10138
|
-
return
|
|
10412
|
+
return fs43.readFileSync(0, "utf-8");
|
|
10139
10413
|
} catch {
|
|
10140
10414
|
return "";
|
|
10141
10415
|
}
|
|
@@ -10210,8 +10484,8 @@ var broadcastCommand = {
|
|
|
10210
10484
|
};
|
|
10211
10485
|
|
|
10212
10486
|
// src/completions/index.ts
|
|
10213
|
-
import
|
|
10214
|
-
import
|
|
10487
|
+
import fs44 from "fs";
|
|
10488
|
+
import path43 from "path";
|
|
10215
10489
|
function completionHandler(current, argv, done) {
|
|
10216
10490
|
const rawArgs = argv._;
|
|
10217
10491
|
const args = rawArgs.slice(1, -1);
|
|
@@ -10315,7 +10589,7 @@ function completeTreeRemoveList(command, args, current, config, done) {
|
|
|
10315
10589
|
}
|
|
10316
10590
|
function completeRepoBranches(alias, current, config, done) {
|
|
10317
10591
|
const repoPath = config.repos[alias];
|
|
10318
|
-
if (!repoPath || !
|
|
10592
|
+
if (!repoPath || !fs44.existsSync(repoPath)) {
|
|
10319
10593
|
done([]);
|
|
10320
10594
|
return;
|
|
10321
10595
|
}
|
|
@@ -10324,19 +10598,19 @@ function completeRepoBranches(alias, current, config, done) {
|
|
|
10324
10598
|
done(branches);
|
|
10325
10599
|
}
|
|
10326
10600
|
function completeGroupBranches(groupName, repoAliases, current, config, done) {
|
|
10327
|
-
const groupDir =
|
|
10328
|
-
if (!
|
|
10601
|
+
const groupDir = path43.join(config.worktreesRoot, groupName);
|
|
10602
|
+
if (!fs44.existsSync(groupDir)) {
|
|
10329
10603
|
done([]);
|
|
10330
10604
|
return;
|
|
10331
10605
|
}
|
|
10332
10606
|
try {
|
|
10333
|
-
const branchDirs =
|
|
10334
|
-
const bdPath =
|
|
10607
|
+
const branchDirs = fs44.readdirSync(groupDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
|
|
10608
|
+
const bdPath = path43.join(groupDir, d.name);
|
|
10335
10609
|
for (const alias of repoAliases) {
|
|
10336
10610
|
const repoPath = config.repos[alias];
|
|
10337
10611
|
if (!repoPath) continue;
|
|
10338
|
-
const subPath =
|
|
10339
|
-
if (
|
|
10612
|
+
const subPath = path43.join(bdPath, path43.basename(repoPath));
|
|
10613
|
+
if (fs44.existsSync(subPath)) {
|
|
10340
10614
|
const branch = getCurrentBranch(subPath);
|
|
10341
10615
|
if (branch) return branch;
|
|
10342
10616
|
}
|
|
@@ -10350,7 +10624,7 @@ function completeGroupBranches(groupName, repoAliases, current, config, done) {
|
|
|
10350
10624
|
}
|
|
10351
10625
|
|
|
10352
10626
|
// src/version.ts
|
|
10353
|
-
var VERSION = true ? "1.
|
|
10627
|
+
var VERSION = true ? "1.7.0" : "dev";
|
|
10354
10628
|
|
|
10355
10629
|
// src/cli.ts
|
|
10356
10630
|
function showHelp() {
|