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