santree 0.4.0 → 0.5.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 +46 -2
- package/dist/commands/dashboard.js +425 -95
- package/dist/commands/doctor.js +103 -17
- package/dist/commands/helpers/statusline.js +10 -2
- package/dist/commands/update.d.ts +15 -0
- package/dist/commands/update.js +72 -0
- package/dist/commands/worktree/diff.d.ts +13 -0
- package/dist/commands/worktree/diff.js +76 -0
- package/dist/lib/ai.d.ts +12 -2
- package/dist/lib/ai.js +37 -6
- package/dist/lib/dashboard/DetailPanel.d.ts +9 -0
- package/dist/lib/dashboard/DetailPanel.js +235 -89
- package/dist/lib/dashboard/DiffOverlay.d.ts +50 -0
- package/dist/lib/dashboard/DiffOverlay.js +243 -0
- package/dist/lib/dashboard/IssueList.d.ts +20 -3
- package/dist/lib/dashboard/IssueList.js +74 -103
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +6 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +4 -7
- package/dist/lib/dashboard/ReviewList.d.ts +3 -1
- package/dist/lib/dashboard/ReviewList.js +3 -3
- package/dist/lib/dashboard/data.js +10 -4
- package/dist/lib/dashboard/theme.d.ts +24 -0
- package/dist/lib/dashboard/theme.js +113 -0
- package/dist/lib/dashboard/types.d.ts +52 -1
- package/dist/lib/dashboard/types.js +81 -0
- package/dist/lib/git.d.ts +20 -0
- package/dist/lib/git.js +37 -0
- package/dist/lib/version.d.ts +55 -0
- package/dist/lib/version.js +224 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -12,6 +12,8 @@ const { version } = require("../../package.json");
|
|
|
12
12
|
import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
|
|
13
13
|
import { getAuthStatus, getValidTokens } from "../lib/linear.js";
|
|
14
14
|
import { getMultiplexer } from "../lib/multiplexer/index.js";
|
|
15
|
+
import { resolveClaudeBinary } from "../lib/ai.js";
|
|
16
|
+
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
|
|
15
17
|
const execAsync = promisify(exec);
|
|
16
18
|
export const description = "Check system requirements and integrations";
|
|
17
19
|
/**
|
|
@@ -107,9 +109,12 @@ async function checkMultiplexer() {
|
|
|
107
109
|
}
|
|
108
110
|
const version = await tryExec("cmux --version 2>/dev/null");
|
|
109
111
|
const ping = await tryExec("cmux ping 2>/dev/null");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// Note: cmux #1472 (programmatic workspaces with dead PTYs) is a real
|
|
113
|
+
// limitation but only surfaces when a specific dashboard flow tries to
|
|
114
|
+
// auto-execute a command in a freshly-created workspace. Showing it on
|
|
115
|
+
// every doctor run made cmux look broken when it isn't — the limitation
|
|
116
|
+
// is documented in CLAUDE.md and the README. We only flag a hint here
|
|
117
|
+
// when cmux is actually unreachable.
|
|
113
118
|
return {
|
|
114
119
|
name: "cmux",
|
|
115
120
|
description,
|
|
@@ -117,7 +122,40 @@ async function checkMultiplexer() {
|
|
|
117
122
|
installed: !!ping,
|
|
118
123
|
version: version || "unknown",
|
|
119
124
|
path,
|
|
120
|
-
hint
|
|
125
|
+
hint: !ping
|
|
126
|
+
? "cmux app not reachable — open cmux.app or set SANTREE_MULTIPLEXER=tmux."
|
|
127
|
+
: undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Checks the Claude CLI, preferring cmux's bundled binary when running inside
|
|
132
|
+
* cmux. The standard `checkTool` uses `which claude` which can't locate the
|
|
133
|
+
* cmux shim at /Applications/cmux.app/Contents/Resources/bin/claude — that
|
|
134
|
+
* binary isn't on PATH. See manaflow-ai/cmux#2048.
|
|
135
|
+
*/
|
|
136
|
+
async function checkClaude() {
|
|
137
|
+
const resolved = resolveClaudeBinary();
|
|
138
|
+
const inCmux = getMultiplexer().kind === "cmux";
|
|
139
|
+
const description = inCmux ? "Claude Code CLI (cmux-bundled)" : "Claude Code CLI";
|
|
140
|
+
if (!resolved) {
|
|
141
|
+
return {
|
|
142
|
+
name: "claude",
|
|
143
|
+
description,
|
|
144
|
+
required: true,
|
|
145
|
+
installed: false,
|
|
146
|
+
hint: inCmux
|
|
147
|
+
? "Open cmux.app to install its bundled Claude, or install standalone: npm install -g @anthropic-ai/claude-code"
|
|
148
|
+
: "Install: npm install -g @anthropic-ai/claude-code",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const version = await tryExec(`"${resolved}" --version 2>/dev/null | head -1`);
|
|
152
|
+
return {
|
|
153
|
+
name: "claude",
|
|
154
|
+
description,
|
|
155
|
+
required: true,
|
|
156
|
+
installed: true,
|
|
157
|
+
version: version || "unknown",
|
|
158
|
+
path: resolved,
|
|
121
159
|
};
|
|
122
160
|
}
|
|
123
161
|
/**
|
|
@@ -411,7 +449,7 @@ function StatusIcon({ ok, required }) {
|
|
|
411
449
|
return required ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "yellow", children: "\u25CB" });
|
|
412
450
|
}
|
|
413
451
|
function ToolRow({ tool }) {
|
|
414
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
|
|
452
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), tool.latestVersion && tool.version && (_jsxs(Text, { color: isUpdateAvailable(tool.version, tool.latestVersion) ? "yellow" : undefined, dimColor: !isUpdateAvailable(tool.version, tool.latestVersion), children: ["Latest: ", tool.latestVersion, isUpdateAvailable(tool.version, tool.latestVersion) ? " ⬆ update available" : ""] })), tool.path && _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.updateHint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.updateHint] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
|
|
415
453
|
}
|
|
416
454
|
function LinearRow({ linear }) {
|
|
417
455
|
const isOk = linear.authenticated && linear.tokenValid && linear.repoLinked;
|
|
@@ -461,30 +499,78 @@ export default function Doctor() {
|
|
|
461
499
|
const [loading, setLoading] = useState(true);
|
|
462
500
|
useEffect(() => {
|
|
463
501
|
async function runChecks() {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
502
|
+
const pm = detectPackageManager();
|
|
503
|
+
const [results, latestSantree, latestClaude] = await Promise.all([
|
|
504
|
+
Promise.all([
|
|
505
|
+
checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
|
|
506
|
+
checkGhAuth(),
|
|
507
|
+
checkMultiplexer(),
|
|
508
|
+
checkClaude(),
|
|
509
|
+
]),
|
|
510
|
+
getLatestVersionFor(SANTREE_PACKAGE),
|
|
511
|
+
getLatestVersionFor(CLAUDE_CODE_PACKAGE),
|
|
469
512
|
]);
|
|
470
|
-
//
|
|
513
|
+
// Synthetic row for santree itself — surfaces update status.
|
|
514
|
+
const santreeRow = {
|
|
515
|
+
name: "santree",
|
|
516
|
+
description: "Santree CLI (this app)",
|
|
517
|
+
required: true,
|
|
518
|
+
installed: true,
|
|
519
|
+
version: CURRENT_VERSION,
|
|
520
|
+
latestVersion: latestSantree ?? undefined,
|
|
521
|
+
updateHint: latestSantree && isUpdateAvailable(CURRENT_VERSION, latestSantree)
|
|
522
|
+
? "Run: santree update"
|
|
523
|
+
: undefined,
|
|
524
|
+
};
|
|
525
|
+
results.unshift(santreeRow);
|
|
526
|
+
// Augment the claude row with latest-version info from npm registry.
|
|
527
|
+
// When the resolved binary is the cmux-bundled one, npm install can't
|
|
528
|
+
// update it — the bundled binary is shipped inside cmux.app. Show a
|
|
529
|
+
// cmux-aware hint instead of the generic npm command.
|
|
530
|
+
const claudeRow = results.find((r) => r.name === "claude");
|
|
531
|
+
if (claudeRow && claudeRow.installed && latestClaude) {
|
|
532
|
+
claudeRow.latestVersion = latestClaude;
|
|
533
|
+
if (claudeRow.version && isUpdateAvailable(claudeRow.version, latestClaude)) {
|
|
534
|
+
const isCmuxBundled = !!claudeRow.path?.includes("/cmux.app/");
|
|
535
|
+
if (isCmuxBundled) {
|
|
536
|
+
claudeRow.updateHint = "Bundled with cmux — update cmux.app to get the latest Claude.";
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const cmd = getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`);
|
|
540
|
+
claudeRow.updateHint = `Run: ${cmd.display}`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Optional: a syntax-highlighted diff pager — used by `st worktree diff`
|
|
545
|
+
// and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
|
|
546
|
+
// pager works (delta, diff-so-fancy, …); without one set, git's
|
|
547
|
+
// default pager runs. Delta is the most popular choice so we check
|
|
548
|
+
// for it as a convenience, but it is never a hard dependency.
|
|
549
|
+
const deltaCheck = await checkTool("delta", "Recommended diff pager — any pager works", false, "delta --version | head -1", "Optional — git's default pager works too. Set SANTREE_DIFF_TOOL or `git config core.pager <tool>`. To install delta: brew install git-delta");
|
|
550
|
+
results.push(deltaCheck);
|
|
551
|
+
// Optional: a `.code-workspace`-aware editor (VSCode or Cursor).
|
|
552
|
+
// Santree itself works with any editor via $SANTREE_EDITOR — this
|
|
553
|
+
// check exists only because the dashboard's `E workspace` shortcut
|
|
554
|
+
// needs an editor that understands `.code-workspace` files. Missing
|
|
555
|
+
// here just means the shortcut is hidden; everything else still works.
|
|
556
|
+
const workspaceEditorDesc = "Workspace editor (`E workspace` shortcut)";
|
|
471
557
|
const [codeCheck, cursorCheck] = await Promise.all([
|
|
472
|
-
checkTool("code",
|
|
473
|
-
checkTool("cursor",
|
|
558
|
+
checkTool("code", workspaceEditorDesc, false, "code --version | head -1", ""),
|
|
559
|
+
checkTool("cursor", workspaceEditorDesc, false, "cursor --version | head -1", ""),
|
|
474
560
|
]);
|
|
475
561
|
if (codeCheck.installed) {
|
|
476
|
-
results.push(
|
|
562
|
+
results.push(codeCheck);
|
|
477
563
|
}
|
|
478
564
|
else if (cursorCheck.installed) {
|
|
479
|
-
results.push(
|
|
565
|
+
results.push(cursorCheck);
|
|
480
566
|
}
|
|
481
567
|
else {
|
|
482
568
|
results.push({
|
|
483
569
|
name: "code/cursor",
|
|
484
|
-
description:
|
|
570
|
+
description: workspaceEditorDesc,
|
|
485
571
|
required: false,
|
|
486
572
|
installed: false,
|
|
487
|
-
hint: "
|
|
573
|
+
hint: "Optional — santree works with any $SANTREE_EDITOR. Only needed for the dashboard's `.code-workspace` shortcut.",
|
|
488
574
|
});
|
|
489
575
|
}
|
|
490
576
|
const linearResult = await checkLinearAuth();
|
|
@@ -132,9 +132,17 @@ function getGitChanges(cwd) {
|
|
|
132
132
|
untracked: countLines(git(cwd, "ls-files --others --exclude-standard")),
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
-
// Build a progress bar for context usage
|
|
135
|
+
// Build a progress bar for context usage.
|
|
136
|
+
//
|
|
137
|
+
// We deliberately inflate the displayed percentage by 20% (clamped to 100) so
|
|
138
|
+
// the bar fills up faster than the model's actual context window. The point is
|
|
139
|
+
// to nudge toward more-frequent /compact: the color thresholds (60%/80%) trip
|
|
140
|
+
// earlier, so the yellow/red warnings show up while there's still real headroom
|
|
141
|
+
// left to compact gracefully instead of after the model has already started
|
|
142
|
+
// dropping content.
|
|
143
|
+
const CONTEXT_DISPLAY_MULTIPLIER = 1.2;
|
|
136
144
|
function formatContextUsage(usedPercentage) {
|
|
137
|
-
const used = Math.round(usedPercentage);
|
|
145
|
+
const used = Math.min(100, Math.round(usedPercentage * CONTEXT_DISPLAY_MULTIPLIER));
|
|
138
146
|
const color = used >= 80 ? c.red : used >= 60 ? c.yellow : c.green;
|
|
139
147
|
const width = 20;
|
|
140
148
|
const filled = Math.round((used * width) / 100);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Update santree to the latest version on npm";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
pm: z.ZodOptional<z.ZodEnum<{
|
|
6
|
+
npm: "npm";
|
|
7
|
+
pnpm: "pnpm";
|
|
8
|
+
yarn: "yarn";
|
|
9
|
+
}>>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
type Props = {
|
|
12
|
+
options: z.infer<typeof options>;
|
|
13
|
+
};
|
|
14
|
+
export default function Update({ options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text, Box, useApp } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, getLatestVersion, getLatestVersionFor, getInstalledClaudeVersion, isUpdateAvailable, detectPackageManager, getInstallCommand, getInstallCommandFor, } from "../lib/version.js";
|
|
7
|
+
import { spawnAsync } from "../lib/exec.js";
|
|
8
|
+
export const description = "Update santree to the latest version on npm";
|
|
9
|
+
export const options = z.object({
|
|
10
|
+
force: z.boolean().optional().describe("Reinstall even if already on the latest version"),
|
|
11
|
+
pm: z
|
|
12
|
+
.enum(["npm", "pnpm", "yarn"])
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Override package manager auto-detection"),
|
|
15
|
+
});
|
|
16
|
+
const TAIL_LINES = 8;
|
|
17
|
+
function tail(text, n) {
|
|
18
|
+
return text
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter((l) => l.length > 0)
|
|
21
|
+
.slice(-n);
|
|
22
|
+
}
|
|
23
|
+
export default function Update({ options }) {
|
|
24
|
+
const { exit } = useApp();
|
|
25
|
+
const [status, setStatus] = useState("checking");
|
|
26
|
+
const [latest, setLatest] = useState(null);
|
|
27
|
+
const [pm, setPm] = useState("npm");
|
|
28
|
+
const [installCmd, setInstallCmd] = useState("");
|
|
29
|
+
const [output, setOutput] = useState("");
|
|
30
|
+
const [error, setError] = useState(null);
|
|
31
|
+
const [claude, setClaude] = useState(null);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
(async () => {
|
|
34
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
35
|
+
// Check santree + claude versions in parallel.
|
|
36
|
+
const [latestVersion, latestClaude] = await Promise.all([
|
|
37
|
+
getLatestVersion({ force: true }),
|
|
38
|
+
getLatestVersionFor(CLAUDE_CODE_PACKAGE, { force: true }),
|
|
39
|
+
]);
|
|
40
|
+
setLatest(latestVersion);
|
|
41
|
+
setClaude({ installed: getInstalledClaudeVersion(), latest: latestClaude });
|
|
42
|
+
const detectedPm = options.pm ?? detectPackageManager();
|
|
43
|
+
setPm(detectedPm);
|
|
44
|
+
const cmd = getInstallCommand(detectedPm);
|
|
45
|
+
setInstallCmd(cmd.display);
|
|
46
|
+
if (!latestVersion) {
|
|
47
|
+
setStatus("error");
|
|
48
|
+
setError("Could not reach the npm registry. Check your connection.");
|
|
49
|
+
setTimeout(() => exit(), 100);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!options.force && !isUpdateAvailable(CURRENT_VERSION, latestVersion)) {
|
|
53
|
+
setStatus("up-to-date");
|
|
54
|
+
setTimeout(() => exit(), 100);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setStatus("installing");
|
|
58
|
+
const result = await spawnAsync(cmd.cmd, cmd.args, {
|
|
59
|
+
onOutput: (data) => setOutput(data),
|
|
60
|
+
});
|
|
61
|
+
if (result.code === 0) {
|
|
62
|
+
setStatus("done");
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
setStatus("error");
|
|
66
|
+
setError(`${cmd.display} exited with code ${result.code}`);
|
|
67
|
+
}
|
|
68
|
+
setTimeout(() => exit(), 100);
|
|
69
|
+
})();
|
|
70
|
+
}, []);
|
|
71
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Santree Update" }) }), _jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "current:" }), _jsxs(Text, { children: ["v", CURRENT_VERSION] })] }), latest && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "latest: " }), _jsxs(Text, { color: isUpdateAvailable(CURRENT_VERSION, latest) ? "yellow" : "green", children: ["v", latest] })] })), installCmd && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "via: " }), _jsxs(Text, { children: [installCmd, " ", _jsxs(Text, { dimColor: true, children: ["(detected: ", pm, ")"] })] })] }))] }), _jsxs(Box, { marginTop: 1, children: [status === "checking" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: "Checking npm registry..." })] })), status === "installing" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: ["Running ", installCmd, "..."] })] })), status === "up-to-date" && (_jsx(Text, { color: "green", bold: true, children: "\u2713 Already on the latest version" })), status === "done" && latest && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Updated to v", latest] })), status === "error" && error && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }), (status === "installing" || status === "error") && output && (_jsx(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: tail(output, TAIL_LINES).map((line, i) => (_jsx(Text, { dimColor: true, children: line }, i))) })), claude && claude.installed && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: claude.latest && isUpdateAvailable(claude.installed, claude.latest) ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: ["\u2B06 Claude Code ", claude.installed, " \u2192 ", claude.latest, " available"] }), _jsxs(Text, { dimColor: true, children: ["Run: ", getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`).display] })] })) : claude.latest ? (_jsxs(Text, { dimColor: true, children: ["\u2713 Claude Code ", claude.installed, " is up to date"] })) : null }))] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "View worktree diff against its base branch (uses delta if installed)";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
staged: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
unstaged: z.ZodOptional<z.ZodBoolean>;
|
|
6
|
+
commits: z.ZodOptional<z.ZodBoolean>;
|
|
7
|
+
base: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
type Props = {
|
|
10
|
+
options: z.infer<typeof options>;
|
|
11
|
+
};
|
|
12
|
+
export default function Diff({ options: opts }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text, Box, useApp } from "ink";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { findRepoRoot, getCurrentBranch, getBaseBranch, getDiffTool } from "../../lib/git.js";
|
|
7
|
+
import { run } from "../../lib/exec.js";
|
|
8
|
+
export const description = "View worktree diff against its base branch (uses delta if installed)";
|
|
9
|
+
export const options = z.object({
|
|
10
|
+
staged: z.boolean().optional().describe("Show only staged changes"),
|
|
11
|
+
unstaged: z.boolean().optional().describe("Show only unstaged changes (working tree vs index)"),
|
|
12
|
+
commits: z.boolean().optional().describe("Show only committed changes (base...HEAD)"),
|
|
13
|
+
base: z.string().optional().describe("Override base branch"),
|
|
14
|
+
});
|
|
15
|
+
export default function Diff({ options: opts }) {
|
|
16
|
+
const [status, setStatus] = useState({ state: "running" });
|
|
17
|
+
const { exit } = useApp();
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const repoRoot = findRepoRoot();
|
|
20
|
+
if (!repoRoot) {
|
|
21
|
+
setStatus({ state: "error", message: "Not inside a git repository" });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const branch = getCurrentBranch();
|
|
25
|
+
if (!branch) {
|
|
26
|
+
setStatus({ state: "error", message: "Could not determine current branch" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const baseBranch = opts.base ?? getBaseBranch(branch);
|
|
30
|
+
// Use merge-base (not base tip) so upstream-only changes are excluded —
|
|
31
|
+
// matches GitHub PR diff semantics. Falls back to baseBranch if merge-base
|
|
32
|
+
// can't be resolved (e.g. unrelated histories).
|
|
33
|
+
const mergeBase = run(`git -C "${repoRoot}" merge-base "${baseBranch}" HEAD`) ?? baseBranch;
|
|
34
|
+
// Resolve diff range based on flags. Defaults to merge-base..working-tree
|
|
35
|
+
// (everything on this branch including uncommitted work, branch-only).
|
|
36
|
+
// Honor SANTREE_DIFF_TOOL by overriding core.pager just for this invocation
|
|
37
|
+
// — `-c` config takes precedence over the user's global git config.
|
|
38
|
+
const tool = getDiffTool();
|
|
39
|
+
const args = ["-C", repoRoot];
|
|
40
|
+
if (tool) {
|
|
41
|
+
args.push("-c", `core.pager=${tool}`);
|
|
42
|
+
}
|
|
43
|
+
args.push("diff");
|
|
44
|
+
if (opts.staged) {
|
|
45
|
+
args.push("--staged");
|
|
46
|
+
}
|
|
47
|
+
else if (opts.unstaged) {
|
|
48
|
+
// working tree vs index — no extra arg
|
|
49
|
+
}
|
|
50
|
+
else if (opts.commits) {
|
|
51
|
+
args.push(`${mergeBase}..HEAD`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
args.push(mergeBase);
|
|
55
|
+
}
|
|
56
|
+
const child = spawn("git", args, { stdio: "inherit" });
|
|
57
|
+
child.on("error", (err) => {
|
|
58
|
+
setStatus({ state: "error", message: err.message });
|
|
59
|
+
exit();
|
|
60
|
+
});
|
|
61
|
+
child.on("close", (code) => {
|
|
62
|
+
setStatus({ state: "done", exitCode: code ?? 0 });
|
|
63
|
+
exit();
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
if (!child.killed)
|
|
67
|
+
child.kill();
|
|
68
|
+
};
|
|
69
|
+
}, []);
|
|
70
|
+
if (status.state === "error") {
|
|
71
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["\u2717 ", status.message] }) }));
|
|
72
|
+
}
|
|
73
|
+
// While running: render nothing so git/delta own the terminal.
|
|
74
|
+
// On done: render nothing — git's output already filled the screen.
|
|
75
|
+
return null;
|
|
76
|
+
}
|
package/dist/lib/ai.d.ts
CHANGED
|
@@ -37,8 +37,18 @@ export declare function fetchAndRenderPR(branch: string): Promise<string | null>
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function fetchAndRenderDiff(branch: string): Promise<string>;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* Resolve the path to the Claude CLI binary, preferring cmux's bundled copy
|
|
41
|
+
* when running inside cmux. Falls back to PATH lookup, then to Anthropic's
|
|
42
|
+
* standard installer location (`~/.claude/local/claude`). Returns null if
|
|
43
|
+
* none of those resolve.
|
|
44
|
+
*
|
|
45
|
+
* Used by every santree code path that needs to invoke or report the Claude
|
|
46
|
+
* binary — version display, doctor checks, and interactive launches.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveClaudeBinary(): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated Use `resolveClaudeBinary()` directly. Kept as an alias because
|
|
51
|
+
* existing call sites pass the return value straight to spawn args.
|
|
42
52
|
*/
|
|
43
53
|
export declare function resolveAgentBinary(): string | null;
|
|
44
54
|
/**
|
package/dist/lib/ai.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execSync, spawn, spawnSync } from "child_process";
|
|
2
|
-
import { writeFileSync } from "fs";
|
|
2
|
+
import { existsSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { tmpdir } from "os";
|
|
4
|
+
import { homedir, tmpdir } from "os";
|
|
5
|
+
import { getMultiplexer } from "./multiplexer/index.js";
|
|
5
6
|
import { getCurrentBranch, extractTicketId, findRepoRoot, findMainRepoRoot, getBaseBranch, } from "./git.js";
|
|
6
7
|
import { renderPrompt, renderTicket, renderDiff, renderPR } from "./prompts.js";
|
|
7
8
|
import { getTicketContent, cleanupImages } from "./linear.js";
|
|
@@ -105,17 +106,47 @@ export async function fetchAndRenderDiff(branch) {
|
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
109
|
+
* cmux ships its own Claude CLI shim wired to the active cmux workspace. When
|
|
110
|
+
* we run inside cmux, the system `claude` (if any) talks to a different
|
|
111
|
+
* session — confusing for the user. See manaflow-ai/cmux#2048.
|
|
110
112
|
*/
|
|
111
|
-
|
|
113
|
+
const CMUX_CLAUDE_PATH = "/Applications/cmux.app/Contents/Resources/bin/claude";
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the path to the Claude CLI binary, preferring cmux's bundled copy
|
|
116
|
+
* when running inside cmux. Falls back to PATH lookup, then to Anthropic's
|
|
117
|
+
* standard installer location (`~/.claude/local/claude`). Returns null if
|
|
118
|
+
* none of those resolve.
|
|
119
|
+
*
|
|
120
|
+
* Used by every santree code path that needs to invoke or report the Claude
|
|
121
|
+
* binary — version display, doctor checks, and interactive launches.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveClaudeBinary() {
|
|
124
|
+
// Inside cmux, the bundled binary is the only one wired to the active
|
|
125
|
+
// workspace. Always prefer it when present.
|
|
126
|
+
if (getMultiplexer().kind === "cmux" && existsSync(CMUX_CLAUDE_PATH)) {
|
|
127
|
+
return CMUX_CLAUDE_PATH;
|
|
128
|
+
}
|
|
129
|
+
// PATH lookup
|
|
112
130
|
try {
|
|
113
131
|
execSync("which claude", { stdio: "ignore" });
|
|
114
132
|
return "claude";
|
|
115
133
|
}
|
|
116
134
|
catch {
|
|
117
|
-
|
|
135
|
+
// fall through
|
|
118
136
|
}
|
|
137
|
+
// Anthropic installer location — Ink renders may not inherit the user's
|
|
138
|
+
// shell PATH, so check this explicitly.
|
|
139
|
+
const localClaude = join(homedir(), ".claude", "local", "claude");
|
|
140
|
+
if (existsSync(localClaude))
|
|
141
|
+
return localClaude;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* @deprecated Use `resolveClaudeBinary()` directly. Kept as an alias because
|
|
146
|
+
* existing call sites pass the return value straight to spawn args.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveAgentBinary() {
|
|
149
|
+
return resolveClaudeBinary();
|
|
119
150
|
}
|
|
120
151
|
// Conservative limit: 200KB leaves room for env vars within macOS 256KB ARG_MAX
|
|
121
152
|
const ARG_MAX_SAFE = 200 * 1024;
|
|
@@ -7,5 +7,14 @@ interface Props {
|
|
|
7
7
|
creatingForTicket: string | null;
|
|
8
8
|
creationLogs: string;
|
|
9
9
|
}
|
|
10
|
+
export type IssueActionItem = {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
color: string;
|
|
14
|
+
};
|
|
15
|
+
/** Returns the context-sensitive action key list for the selected issue.
|
|
16
|
+
* Lifted out of the panel so the dashboard can render it on the same row as
|
|
17
|
+
* the global command bar (so left- and right-pane key hints align). */
|
|
18
|
+
export declare function buildIssueActions(di: DashboardIssue): IssueActionItem[];
|
|
10
19
|
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
20
|
export {};
|