santree 0.7.4 → 0.7.6
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 +2 -3
- package/dist/commands/config.d.ts +12 -0
- package/dist/commands/config.js +444 -0
- package/dist/commands/dashboard.js +187 -80
- package/dist/commands/helpers/statusline.js +0 -30
- package/dist/commands/issue/index.d.ts +1 -1
- package/dist/commands/issue/index.js +1 -1
- package/dist/commands/pr/context.d.ts +7 -0
- package/dist/commands/pr/context.js +36 -0
- package/dist/commands/pr/fix.d.ts +11 -2
- package/dist/commands/pr/fix.js +64 -12
- package/dist/commands/worktree/create.js +17 -11
- package/dist/commands/worktree/open.js +3 -2
- package/dist/commands/worktree/switch.js +3 -11
- package/dist/lib/ai.d.ts +11 -0
- package/dist/lib/ai.js +71 -2
- package/dist/lib/cd-hint.d.ts +18 -0
- package/dist/lib/cd-hint.js +27 -0
- package/dist/lib/claude-config.d.ts +26 -0
- package/dist/lib/claude-config.js +121 -0
- package/dist/lib/config/TrackerPicker.d.ts +10 -0
- package/dist/lib/config/TrackerPicker.js +113 -0
- package/dist/lib/config/diagnostics.d.ts +37 -0
- package/dist/lib/config/diagnostics.js +190 -0
- package/dist/lib/config-store.d.ts +23 -0
- package/dist/lib/config-store.js +68 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +7 -2
- package/dist/lib/dashboard/DetailPanel.js +54 -37
- package/dist/lib/dashboard/IssueList.d.ts +5 -1
- package/dist/lib/dashboard/IssueList.js +24 -5
- package/dist/lib/dashboard/MultilineTextArea.js +1 -1
- package/dist/lib/dashboard/Overlays.js +3 -4
- package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
- package/dist/lib/dashboard/ReviewList.js +6 -3
- package/dist/lib/dashboard/data.js +7 -34
- package/dist/lib/dashboard/external-editor.d.ts +1 -1
- package/dist/lib/dashboard/external-editor.js +3 -2
- package/dist/lib/dashboard/types.d.ts +19 -2
- package/dist/lib/dashboard/types.js +68 -2
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +11 -1
- package/dist/lib/fix-loop.d.ts +45 -0
- package/dist/lib/fix-loop.js +83 -0
- package/dist/lib/git.d.ts +3 -29
- package/dist/lib/git.js +6 -62
- package/dist/lib/github.d.ts +70 -0
- package/dist/lib/github.js +128 -0
- package/dist/lib/multiplexer/cmux.js +143 -17
- package/dist/lib/multiplexer/none.js +3 -3
- package/dist/lib/multiplexer/tmux.js +5 -29
- package/dist/lib/multiplexer/types.d.ts +34 -1
- package/dist/lib/open-url.d.ts +3 -2
- package/dist/lib/open-url.js +5 -4
- package/dist/lib/prompts.d.ts +35 -1
- package/dist/lib/prompts.js +7 -0
- package/dist/lib/setup/apply.d.ts +16 -0
- package/dist/lib/setup/apply.js +35 -0
- package/dist/lib/setup/gitignore.d.ts +24 -0
- package/dist/lib/setup/gitignore.js +95 -0
- package/dist/lib/setup/steps.d.ts +39 -0
- package/dist/lib/setup/steps.js +333 -0
- package/dist/lib/setup/tools.d.ts +23 -0
- package/dist/lib/setup/tools.js +47 -0
- package/dist/lib/squirrel-loader.d.ts +3 -1
- package/dist/lib/squirrel-loader.js +2 -2
- package/dist/lib/trackers/github/index.js +2 -2
- package/dist/lib/trackers/local/store.js +1 -1
- package/package.json +1 -2
- package/prompts/fix-context.njk +79 -0
- package/prompts/fix-loop.njk +18 -0
- package/prompts/pr.njk +1 -1
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -618
- package/dist/commands/github/auth.d.ts +0 -2
- package/dist/commands/github/auth.js +0 -56
- package/dist/commands/github/index.d.ts +0 -1
- package/dist/commands/github/index.js +0 -1
- package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
- package/dist/commands/helpers/english-tutor/index.js +0 -1
- package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
- package/dist/commands/helpers/english-tutor/install.js +0 -24
- package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/prompt.js +0 -16
- package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/session-start.js +0 -34
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
- package/dist/commands/helpers/session-signal/end.d.ts +0 -2
- package/dist/commands/helpers/session-signal/end.js +0 -13
- package/dist/commands/helpers/session-signal/index.d.ts +0 -1
- package/dist/commands/helpers/session-signal/index.js +0 -1
- package/dist/commands/helpers/session-signal/install.d.ts +0 -8
- package/dist/commands/helpers/session-signal/install.js +0 -24
- package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
- package/dist/commands/helpers/session-signal/notification.js +0 -13
- package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
- package/dist/commands/helpers/session-signal/prompt.js +0 -13
- package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
- package/dist/commands/helpers/session-signal/stop.js +0 -13
- package/dist/commands/helpers/shell-init.d.ts +0 -11
- package/dist/commands/helpers/shell-init.js +0 -122
- package/dist/commands/issue/setup.d.ts +0 -2
- package/dist/commands/issue/setup.js +0 -108
- package/dist/commands/issue/switch.d.ts +0 -12
- package/dist/commands/issue/switch.js +0 -38
- package/dist/commands/linear/switch.d.ts +0 -2
- package/dist/commands/linear/switch.js +0 -75
- package/dist/lib/english-tutor.d.ts +0 -13
- package/dist/lib/english-tutor.js +0 -125
- package/dist/lib/session-signal.d.ts +0 -16
- package/dist/lib/session-signal.js +0 -104
- package/prompts/english-tutor-prompt.njk +0 -15
- package/shell/init.bash.njk +0 -129
- package/shell/init.zsh.njk +0 -204
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Text, Box, useInput } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { setRepoTracker, getIssueTracker } from "../trackers/index.js";
|
|
6
|
+
import { setRepoLinearOrg, startOAuthFlow } from "../trackers/linear/index.js";
|
|
7
|
+
import { readLinearAuthStore } from "../trackers/auth-store.js";
|
|
8
|
+
import { getAuthenticatedUser, getCurrentRepoNwo } from "../trackers/github/auth.js";
|
|
9
|
+
const AUTH_NEW = "__auth_new__";
|
|
10
|
+
const TRACKERS = [
|
|
11
|
+
{ kind: "local", label: "Local", hint: "built-in, file-based — no account needed" },
|
|
12
|
+
{ kind: "linear", label: "Linear", hint: "OAuth workspace" },
|
|
13
|
+
{ kind: "github", label: "GitHub", hint: "GitHub Issues via gh CLI" },
|
|
14
|
+
];
|
|
15
|
+
export default function TrackerPicker({ repoRoot, activeKind, activeOrg, onApplied, onCancel, }) {
|
|
16
|
+
const [phase, setPhase] = useState("pick-tracker");
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const startIdx = Math.max(0, TRACKERS.findIndex((t) => t.kind === activeKind));
|
|
19
|
+
const [trackerIdx, setTrackerIdx] = useState(startIdx);
|
|
20
|
+
const [orgs, setOrgs] = useState([]);
|
|
21
|
+
const [orgIdx, setOrgIdx] = useState(0);
|
|
22
|
+
function authenticateNewLinear() {
|
|
23
|
+
setPhase("authenticating");
|
|
24
|
+
startOAuthFlow().then((result) => {
|
|
25
|
+
if (!result) {
|
|
26
|
+
setError("Authentication failed or timed out. Please try again.");
|
|
27
|
+
setPhase("error");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
31
|
+
setRepoTracker(repoRoot, "linear");
|
|
32
|
+
onApplied(`Authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function chooseTracker(kind) {
|
|
36
|
+
if (kind === "local") {
|
|
37
|
+
setRepoTracker(repoRoot, "local");
|
|
38
|
+
onApplied(`Active tracker: ${getIssueTracker(repoRoot).displayName}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (kind === "linear") {
|
|
42
|
+
const store = readLinearAuthStore();
|
|
43
|
+
const list = Object.entries(store).map(([slug, tokens]) => ({
|
|
44
|
+
slug,
|
|
45
|
+
name: tokens.org_name,
|
|
46
|
+
}));
|
|
47
|
+
if (list.length === 0) {
|
|
48
|
+
authenticateNewLinear();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setOrgs(list);
|
|
52
|
+
const cur = list.findIndex((o) => o.slug === activeOrg);
|
|
53
|
+
setOrgIdx(cur >= 0 ? cur : 0);
|
|
54
|
+
setPhase("pick-org");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// github
|
|
58
|
+
setPhase("checking");
|
|
59
|
+
const user = await getAuthenticatedUser();
|
|
60
|
+
if (!user) {
|
|
61
|
+
setError("GitHub CLI not authenticated. Run: gh auth login");
|
|
62
|
+
setPhase("error");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setRepoTracker(repoRoot, "github");
|
|
66
|
+
const nwo = await getCurrentRepoNwo(repoRoot);
|
|
67
|
+
onApplied(`Active tracker: GitHub (@${user.login}${nwo ? ` · ${nwo}` : ""})`);
|
|
68
|
+
}
|
|
69
|
+
useInput((_input, key) => {
|
|
70
|
+
if (phase === "error") {
|
|
71
|
+
// Any key returns to the tracker menu.
|
|
72
|
+
setError(null);
|
|
73
|
+
setPhase("pick-tracker");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (phase === "pick-tracker") {
|
|
77
|
+
if (key.escape || key.leftArrow)
|
|
78
|
+
onCancel();
|
|
79
|
+
else if (key.upArrow)
|
|
80
|
+
setTrackerIdx((i) => Math.max(0, i - 1));
|
|
81
|
+
else if (key.downArrow)
|
|
82
|
+
setTrackerIdx((i) => Math.min(TRACKERS.length - 1, i + 1));
|
|
83
|
+
else if (key.return)
|
|
84
|
+
void chooseTracker(TRACKERS[trackerIdx].kind);
|
|
85
|
+
}
|
|
86
|
+
else if (phase === "pick-org") {
|
|
87
|
+
const max = orgs.length; // index === orgs.length selects AUTH_NEW
|
|
88
|
+
if (key.leftArrow || key.escape)
|
|
89
|
+
setPhase("pick-tracker");
|
|
90
|
+
else if (key.upArrow)
|
|
91
|
+
setOrgIdx((i) => Math.max(0, i - 1));
|
|
92
|
+
else if (key.downArrow)
|
|
93
|
+
setOrgIdx((i) => Math.min(max, i + 1));
|
|
94
|
+
else if (key.return) {
|
|
95
|
+
if (orgIdx === orgs.length) {
|
|
96
|
+
authenticateNewLinear();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const org = orgs[orgIdx];
|
|
100
|
+
setRepoLinearOrg(repoRoot, org.slug);
|
|
101
|
+
setRepoTracker(repoRoot, "linear");
|
|
102
|
+
onApplied(`Linked to ${org.name} (${org.slug})`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return (_jsxs(Box, { flexDirection: "column", children: [phase === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking\u2026" })] })), phase === "authenticating" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Authenticating with Linear \u2014 follow the browser prompt\u2026" })] })), phase === "pick-tracker" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select the issue tracker for this repo:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: TRACKERS.map((t, i) => {
|
|
107
|
+
const active = t.kind === activeKind;
|
|
108
|
+
return (_jsxs(Text, { children: [i === trackerIdx ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), t.label, active ? _jsx(Text, { color: "green", children: " \u25CF in use" }) : null, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", t.hint] })] }, t.kind));
|
|
109
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 select \u00B7 Enter confirm \u00B7 \u2190 / Esc back" }) })] })), phase === "pick-org" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a Linear workspace to link:" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [orgs.map((org, i) => {
|
|
110
|
+
const active = org.slug === activeOrg && activeKind === "linear";
|
|
111
|
+
return (_jsxs(Text, { children: [i === orgIdx ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), org.name, " (", org.slug, ")", active ? _jsx(Text, { color: "green", children: " \u25CF in use" }) : null] }, org.slug));
|
|
112
|
+
}), _jsxs(Text, { children: [orgIdx === orgs.length ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), _jsx(Text, { dimColor: true, children: "+ Authenticate a new workspace\u2026" })] }, AUTH_NEW)] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 select \u00B7 \u2190 back \u00B7 Enter confirm" }) })] })), phase === "error" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }), _jsx(Text, { dimColor: true, children: "Press any key to go back." })] }))] }));
|
|
113
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only diagnostics for `santree config`. These are the facts santree can
|
|
3
|
+
* *report* but not change from the panel (tool versions, update availability,
|
|
4
|
+
* auth state). The panel renders them as non-selectable info rows; `--check`
|
|
5
|
+
* prints them flat. Configurable settings live in `lib/setup/steps.ts` instead.
|
|
6
|
+
*/
|
|
7
|
+
/** A read-only status row — santree reports it, you can't toggle it here. */
|
|
8
|
+
export interface InfoRow {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
scope: "system" | "global" | "repo";
|
|
13
|
+
required: boolean;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
/** Dim sub-lines (version, path, account, …). */
|
|
16
|
+
lines: string[];
|
|
17
|
+
/** Yellow call-to-action when not ok. */
|
|
18
|
+
hint?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extra detail attached to a configurable step row by id (version / auth / a
|
|
22
|
+
* yellow update hint). Lets an already-configured step render doctor-style
|
|
23
|
+
* without the step catalog itself doing network I/O.
|
|
24
|
+
*/
|
|
25
|
+
export interface StepDetail {
|
|
26
|
+
lines: string[];
|
|
27
|
+
hint?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface DiagnosticsData {
|
|
30
|
+
infoRows: InfoRow[];
|
|
31
|
+
/** Keyed by SetupStep id (gh, claude, tracker). */
|
|
32
|
+
stepDetail: Map<string, StepDetail>;
|
|
33
|
+
/** True when an active multiplexer was detected (tmux/cmux). */
|
|
34
|
+
muxActive: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Run every read-only check. Network calls (npm registry, gh, tracker) run in parallel. */
|
|
37
|
+
export declare function loadDiagnostics(): Promise<DiagnosticsData>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { run } from "../exec.js";
|
|
2
|
+
import { which } from "../setup/tools.js";
|
|
3
|
+
import { getMultiplexer } from "../multiplexer/index.js";
|
|
4
|
+
import { resolveClaudeBinary } from "../ai.js";
|
|
5
|
+
import { findMainRepoRoot } from "../git.js";
|
|
6
|
+
import { getIssueTracker } from "../trackers/index.js";
|
|
7
|
+
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../version.js";
|
|
8
|
+
function tryRun(cmd) {
|
|
9
|
+
return run(`${cmd} 2>/dev/null`);
|
|
10
|
+
}
|
|
11
|
+
function checkGit() {
|
|
12
|
+
const p = which("git");
|
|
13
|
+
if (!p) {
|
|
14
|
+
return {
|
|
15
|
+
id: "git",
|
|
16
|
+
title: "git",
|
|
17
|
+
description: "Version control",
|
|
18
|
+
scope: "system",
|
|
19
|
+
required: true,
|
|
20
|
+
ok: false,
|
|
21
|
+
lines: [],
|
|
22
|
+
hint: "Install: brew install git",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const version = tryRun("git --version | head -1");
|
|
26
|
+
return {
|
|
27
|
+
id: "git",
|
|
28
|
+
title: "git",
|
|
29
|
+
description: "Version control",
|
|
30
|
+
scope: "system",
|
|
31
|
+
required: true,
|
|
32
|
+
ok: true,
|
|
33
|
+
lines: [`Version: ${version || "unknown"}`, `Path: ${p}`],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function checkNode() {
|
|
37
|
+
const version = process.versions.node;
|
|
38
|
+
const major = Number(version.split(".")[0] || "0");
|
|
39
|
+
const ok = major >= 20;
|
|
40
|
+
return {
|
|
41
|
+
id: "node",
|
|
42
|
+
title: "node",
|
|
43
|
+
description: "JavaScript runtime (>= 20 required)",
|
|
44
|
+
scope: "system",
|
|
45
|
+
required: true,
|
|
46
|
+
ok,
|
|
47
|
+
lines: [`Version: v${version}`, `Path: ${process.execPath}`],
|
|
48
|
+
hint: ok ? undefined : "Santree needs Node.js >= 20. Upgrade: https://nodejs.org",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function checkSantreeSelf() {
|
|
52
|
+
const latest = await getLatestVersionFor(SANTREE_PACKAGE);
|
|
53
|
+
const updatable = !!latest && isUpdateAvailable(CURRENT_VERSION, latest);
|
|
54
|
+
const lines = [`Version: ${CURRENT_VERSION}`];
|
|
55
|
+
if (latest)
|
|
56
|
+
lines.push(`Latest: ${latest}${updatable ? " ⬆ update available" : ""}`);
|
|
57
|
+
return {
|
|
58
|
+
id: "santree",
|
|
59
|
+
title: "santree",
|
|
60
|
+
description: "Santree CLI (this app)",
|
|
61
|
+
scope: "system",
|
|
62
|
+
required: true,
|
|
63
|
+
ok: true,
|
|
64
|
+
lines,
|
|
65
|
+
hint: updatable ? "Run: santree update" : undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function checkMultiplexer() {
|
|
69
|
+
const mux = getMultiplexer();
|
|
70
|
+
if (mux.kind === "tmux") {
|
|
71
|
+
const version = tryRun("tmux -V");
|
|
72
|
+
return {
|
|
73
|
+
id: "multiplexer",
|
|
74
|
+
title: "tmux",
|
|
75
|
+
description: "Active multiplexer — new-window flows enabled",
|
|
76
|
+
scope: "system",
|
|
77
|
+
required: false,
|
|
78
|
+
ok: true,
|
|
79
|
+
lines: [`Version: ${version || "unknown"}`, `Path: ${which("tmux") || "unknown"}`],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// cmux
|
|
83
|
+
const version = tryRun("cmux --version");
|
|
84
|
+
const ping = tryRun("cmux ping");
|
|
85
|
+
return {
|
|
86
|
+
id: "multiplexer",
|
|
87
|
+
title: "cmux",
|
|
88
|
+
description: "Active multiplexer — new-window flows enabled",
|
|
89
|
+
scope: "system",
|
|
90
|
+
required: false,
|
|
91
|
+
ok: !!ping,
|
|
92
|
+
lines: [`Version: ${version || "unknown"}`, `Path: ${which("cmux") || "unknown"}`],
|
|
93
|
+
hint: ping ? undefined : "cmux app not reachable — open cmux.app.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function checkWorkspaceEditor() {
|
|
97
|
+
const code = which("code");
|
|
98
|
+
const cursor = which("cursor");
|
|
99
|
+
const found = code ? "code" : cursor ? "cursor" : null;
|
|
100
|
+
if (found) {
|
|
101
|
+
const version = tryRun(`${found} --version | head -1`);
|
|
102
|
+
return {
|
|
103
|
+
id: "workspace-editor",
|
|
104
|
+
title: "Workspace editor",
|
|
105
|
+
description: "code/cursor — powers the dashboard's `E workspace` shortcut",
|
|
106
|
+
scope: "system",
|
|
107
|
+
required: false,
|
|
108
|
+
ok: true,
|
|
109
|
+
lines: [`Editor: ${found}`, `Version: ${version || "unknown"}`],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: "workspace-editor",
|
|
114
|
+
title: "Workspace editor",
|
|
115
|
+
description: "code/cursor — powers the dashboard's `E workspace` shortcut",
|
|
116
|
+
scope: "system",
|
|
117
|
+
required: false,
|
|
118
|
+
ok: false,
|
|
119
|
+
lines: [],
|
|
120
|
+
hint: "Optional — only the `.code-workspace` shortcut needs code/cursor. Everything else uses your configured editor.",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Version + auth detail for the gh and claude step rows. */
|
|
124
|
+
async function loadStepDetail() {
|
|
125
|
+
const detail = new Map();
|
|
126
|
+
const pm = detectPackageManager();
|
|
127
|
+
// claude — version + latest + cmux-aware update hint
|
|
128
|
+
const resolved = resolveClaudeBinary();
|
|
129
|
+
if (resolved) {
|
|
130
|
+
const version = tryRun(`"${resolved}" --version | head -1`);
|
|
131
|
+
const latest = await getLatestVersionFor(CLAUDE_CODE_PACKAGE);
|
|
132
|
+
const lines = [`Version: ${version || "unknown"}`, `Path: ${resolved}`];
|
|
133
|
+
let hint;
|
|
134
|
+
if (latest && version && isUpdateAvailable(version, latest)) {
|
|
135
|
+
lines.push(`Latest: ${latest} ⬆ update available`);
|
|
136
|
+
if (resolved.includes("/cmux.app/")) {
|
|
137
|
+
hint = "Bundled with cmux — update cmux.app to get the latest Claude.";
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
hint = `Run: ${getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`).display}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
detail.set("claude", { lines, hint });
|
|
144
|
+
}
|
|
145
|
+
// gh — version + authenticated login
|
|
146
|
+
if (which("gh")) {
|
|
147
|
+
const version = tryRun("gh --version | head -1");
|
|
148
|
+
const login = tryRun("gh api user --jq .login");
|
|
149
|
+
const lines = [`Version: ${version || "unknown"}`];
|
|
150
|
+
if (login)
|
|
151
|
+
lines.push(`Auth: authenticated as ${login}`);
|
|
152
|
+
detail.set("gh", { lines, hint: login ? undefined : "Run: gh auth login" });
|
|
153
|
+
}
|
|
154
|
+
return detail;
|
|
155
|
+
}
|
|
156
|
+
/** Active issue tracker auth detail for the tracker row. */
|
|
157
|
+
async function loadTrackerDetail() {
|
|
158
|
+
const repoRoot = findMainRepoRoot();
|
|
159
|
+
if (!repoRoot)
|
|
160
|
+
return null;
|
|
161
|
+
try {
|
|
162
|
+
const tracker = getIssueTracker(repoRoot);
|
|
163
|
+
const status = await tracker.getAuthStatus(repoRoot);
|
|
164
|
+
const lines = [`Tracker: ${tracker.displayName}`];
|
|
165
|
+
if (status.accountLabel)
|
|
166
|
+
lines.push(`Account: ${status.accountLabel}`);
|
|
167
|
+
if (status.repoLinked !== undefined)
|
|
168
|
+
lines.push(`Repo linked: ${status.repoLinked ? "yes" : "no"}`);
|
|
169
|
+
return { lines, hint: status.authenticated ? undefined : status.hint };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Run every read-only check. Network calls (npm registry, gh, tracker) run in parallel. */
|
|
176
|
+
export async function loadDiagnostics() {
|
|
177
|
+
const muxActive = getMultiplexer().kind !== "none";
|
|
178
|
+
const [santree, stepDetail, trackerDetail] = await Promise.all([
|
|
179
|
+
checkSantreeSelf(),
|
|
180
|
+
loadStepDetail(),
|
|
181
|
+
loadTrackerDetail(),
|
|
182
|
+
]);
|
|
183
|
+
if (trackerDetail)
|
|
184
|
+
stepDetail.set("tracker", trackerDetail);
|
|
185
|
+
const infoRows = [santree, checkNode(), checkGit()];
|
|
186
|
+
if (muxActive)
|
|
187
|
+
infoRows.push(checkMultiplexer());
|
|
188
|
+
infoRows.push(checkWorkspaceEditor());
|
|
189
|
+
return { infoRows, stepDetail, muxActive };
|
|
190
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SantreeConfig {
|
|
2
|
+
/** Editor launched by the dashboard's [e] action and Ctrl+O in text areas. */
|
|
3
|
+
editor?: string;
|
|
4
|
+
/** Unified-diff pager for `worktree diff` and the dashboard [v] overlay. */
|
|
5
|
+
diffTool?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function configStorePath(): string;
|
|
8
|
+
export declare function readConfigStore(): SantreeConfig;
|
|
9
|
+
export declare function setConfigValue<K extends keyof SantreeConfig>(key: K, value: SantreeConfig[K]): void;
|
|
10
|
+
export declare function unsetConfigValue(key: keyof SantreeConfig): void;
|
|
11
|
+
/**
|
|
12
|
+
* Resolved editor command: `SANTREE_EDITOR` env override → config file → none.
|
|
13
|
+
* Call sites keep their own final fallback (e.g. `"code"` for GUI-open flows,
|
|
14
|
+
* the `VISUAL`/`EDITOR`/`vim` chain for in-terminal editing).
|
|
15
|
+
*/
|
|
16
|
+
export declare function getConfiguredEditor(): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Resolved diff pager: `SANTREE_DIFF_TOOL` env override → config file → none.
|
|
19
|
+
* Restricted to a safe shell-token character set since it ends up in spawn()
|
|
20
|
+
* arguments — even though we never use shell:true, a tight surface defends
|
|
21
|
+
* against accidental misconfigurations.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getConfiguredDiffTool(): string | undefined;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Santree's global config file — `$XDG_CONFIG_HOME/santree/config.json`
|
|
6
|
+
* (defaults to `~/.config/santree/config.json`), sitting next to `auth.json`.
|
|
7
|
+
*
|
|
8
|
+
* This is where preferences santree owns live (editor, diff tool) instead of
|
|
9
|
+
* exporting them into the user's shell rc. Writing here takes effect on the
|
|
10
|
+
* next santree run with no shell restart. The matching `SANTREE_EDITOR` /
|
|
11
|
+
* `SANTREE_DIFF_TOOL` env vars still work as a one-off override that wins over
|
|
12
|
+
* the file, so existing shell exports and CI overrides keep behaving.
|
|
13
|
+
*/
|
|
14
|
+
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
15
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "santree", "config.json");
|
|
16
|
+
export function configStorePath() {
|
|
17
|
+
return CONFIG_PATH;
|
|
18
|
+
}
|
|
19
|
+
export function readConfigStore() {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
22
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeConfigStore(cfg) {
|
|
29
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
30
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n");
|
|
31
|
+
}
|
|
32
|
+
export function setConfigValue(key, value) {
|
|
33
|
+
const cfg = readConfigStore();
|
|
34
|
+
cfg[key] = value;
|
|
35
|
+
writeConfigStore(cfg);
|
|
36
|
+
}
|
|
37
|
+
export function unsetConfigValue(key) {
|
|
38
|
+
const cfg = readConfigStore();
|
|
39
|
+
if (!(key in cfg))
|
|
40
|
+
return;
|
|
41
|
+
delete cfg[key];
|
|
42
|
+
writeConfigStore(cfg);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolved editor command: `SANTREE_EDITOR` env override → config file → none.
|
|
46
|
+
* Call sites keep their own final fallback (e.g. `"code"` for GUI-open flows,
|
|
47
|
+
* the `VISUAL`/`EDITOR`/`vim` chain for in-terminal editing).
|
|
48
|
+
*/
|
|
49
|
+
export function getConfiguredEditor() {
|
|
50
|
+
const env = process.env.SANTREE_EDITOR;
|
|
51
|
+
if (env && env.trim())
|
|
52
|
+
return env.trim();
|
|
53
|
+
const stored = readConfigStore().editor;
|
|
54
|
+
return stored && stored.trim() ? stored.trim() : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolved diff pager: `SANTREE_DIFF_TOOL` env override → config file → none.
|
|
58
|
+
* Restricted to a safe shell-token character set since it ends up in spawn()
|
|
59
|
+
* arguments — even though we never use shell:true, a tight surface defends
|
|
60
|
+
* against accidental misconfigurations.
|
|
61
|
+
*/
|
|
62
|
+
export function getConfiguredDiffTool() {
|
|
63
|
+
const raw = process.env.SANTREE_DIFF_TOOL ?? readConfigStore().diffTool;
|
|
64
|
+
if (!raw || !raw.trim())
|
|
65
|
+
return undefined;
|
|
66
|
+
const tool = raw.trim();
|
|
67
|
+
return /^[a-zA-Z0-9_\-/.+]+$/.test(tool) ? tool : undefined;
|
|
68
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Comment, DashboardIssue, DashboardTab, DeleteStatus } from "./types.js";
|
|
1
|
+
import type { Comment, DashboardIssue, DashboardTab, DeleteStatus, FixLoopRuntime } from "./types.js";
|
|
2
2
|
interface Props {
|
|
3
3
|
issue: DashboardIssue | null;
|
|
4
4
|
scrollOffset: number;
|
|
@@ -8,6 +8,11 @@ interface Props {
|
|
|
8
8
|
creationLogs: string;
|
|
9
9
|
/** Deletion progress for the selected worktree, when one is being removed. */
|
|
10
10
|
deleteStatus?: DeleteStatus;
|
|
11
|
+
/** True while a just-created PR is shown optimistically, before a refresh
|
|
12
|
+
* confirms it (renders a "syncing…" hint next to the PR). */
|
|
13
|
+
prSyncing?: boolean;
|
|
14
|
+
/** Active/recent auto-fix loop for the selected ticket, shown as a ⟳ line. */
|
|
15
|
+
fixLoop?: FixLoopRuntime;
|
|
11
16
|
/** Triage mode: hide worktree/PR/checks sections (they never apply to an
|
|
12
17
|
* inbox issue) and show the discussion instead. */
|
|
13
18
|
triage?: boolean;
|
|
@@ -39,5 +44,5 @@ export declare function buildIssueActions(di: DashboardIssue, trackerName: strin
|
|
|
39
44
|
canMutate?: boolean;
|
|
40
45
|
triageInvestigateConfigured?: boolean;
|
|
41
46
|
}): IssueActionItem[];
|
|
42
|
-
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, triage, comments, onCall, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, prSyncing, fixLoop, triage, comments, onCall, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
43
48
|
export {};
|
|
@@ -134,7 +134,7 @@ export function buildIssueActions(di, trackerName, opts) {
|
|
|
134
134
|
items.push({ key: "c", label: "Create PR", color: "cyan" });
|
|
135
135
|
}
|
|
136
136
|
if (pr) {
|
|
137
|
-
items.push({ key: "f", label: "Fix
|
|
137
|
+
items.push({ key: "f", label: "Fix loop", color: "cyan" });
|
|
138
138
|
items.push({ key: "r", label: "Review", color: "cyan" });
|
|
139
139
|
}
|
|
140
140
|
if (issue.url) {
|
|
@@ -158,7 +158,7 @@ function sectionHeader(icon, label, iconColor = "cyan") {
|
|
|
158
158
|
],
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
-
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, triage = false, comments, onCall, }) {
|
|
161
|
+
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, prSyncing = false, fixLoop, triage = false, comments, onCall, }) {
|
|
162
162
|
// Show deletion progress when the selected worktree is being removed.
|
|
163
163
|
if (issue && deleteStatus) {
|
|
164
164
|
const logLines = deleteStatus.logs.split("\n");
|
|
@@ -417,39 +417,8 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
417
417
|
lines.push({ text: ` +${gs.files.length - maxFiles} more`, dim: true });
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
|
-
//
|
|
421
|
-
if (worktree.
|
|
422
|
-
const msg = worktree.sessionMessage
|
|
423
|
-
? `NEEDS INPUT: ${worktree.sessionMessage}`
|
|
424
|
-
: "NEEDS INPUT";
|
|
425
|
-
lines.push({
|
|
426
|
-
text: "",
|
|
427
|
-
segments: [
|
|
428
|
-
{ text: " ◆ ", color: "red" },
|
|
429
|
-
{ text: msg, color: "red", bold: true },
|
|
430
|
-
],
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
else if (worktree.sessionState === "active") {
|
|
434
|
-
lines.push({
|
|
435
|
-
text: "",
|
|
436
|
-
segments: [
|
|
437
|
-
{ text: " ◆ ", color: "green" },
|
|
438
|
-
{ text: "session active", color: "green" },
|
|
439
|
-
],
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
else if (worktree.sessionState === "idle") {
|
|
443
|
-
lines.push({
|
|
444
|
-
text: "",
|
|
445
|
-
segments: [
|
|
446
|
-
{ text: " ◆ ", color: "yellow" },
|
|
447
|
-
{ text: "session idle", color: "yellow" },
|
|
448
|
-
{ text: " (waiting for prompt)", dim: true },
|
|
449
|
-
],
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
else if (worktree.sessionId) {
|
|
420
|
+
// Resumable Claude session (the stored session id), or none.
|
|
421
|
+
if (worktree.sessionId) {
|
|
453
422
|
lines.push({
|
|
454
423
|
text: "",
|
|
455
424
|
segments: [
|
|
@@ -557,28 +526,71 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
557
526
|
{ text: " " },
|
|
558
527
|
{ text: pr.state, color: prColor },
|
|
559
528
|
{ text: draft, dim: true },
|
|
529
|
+
...(prSyncing ? [{ text: " · syncing…", color: "yellow" }] : []),
|
|
560
530
|
],
|
|
561
531
|
});
|
|
562
532
|
if (pr.url) {
|
|
563
533
|
lines.push({ text: ` ${pr.url}`, dim: true });
|
|
564
534
|
}
|
|
535
|
+
if (prSyncing) {
|
|
536
|
+
// Just created from the dashboard — number/url are known, but checks
|
|
537
|
+
// and reviews aren't fetched yet. Show a clear loading line for this
|
|
538
|
+
// one PR (other PRs keep their already-loaded data).
|
|
539
|
+
lines.push({
|
|
540
|
+
text: "",
|
|
541
|
+
segments: [{ text: " ⟳ loading checks & reviews…", color: "yellow" }],
|
|
542
|
+
});
|
|
543
|
+
}
|
|
565
544
|
}
|
|
566
545
|
else if (!isMain) {
|
|
567
546
|
lines.push(sectionHeader("◉", "Pull Request"));
|
|
568
547
|
lines.push({ text: " no PR yet", dim: true });
|
|
569
548
|
}
|
|
549
|
+
// ── Fix loop ──────────────────────────────────────────────────────
|
|
550
|
+
if (!isMain && fixLoop) {
|
|
551
|
+
const { phase } = fixLoop;
|
|
552
|
+
const color = phase === "running"
|
|
553
|
+
? "cyan"
|
|
554
|
+
: phase === "stalled"
|
|
555
|
+
? "yellow"
|
|
556
|
+
: phase === "stopped-clean"
|
|
557
|
+
? "green"
|
|
558
|
+
: "red";
|
|
559
|
+
const label = phase === "running"
|
|
560
|
+
? `fix loop · ${fixLoop.status}`
|
|
561
|
+
: phase === "stalled"
|
|
562
|
+
? "fix loop · stalled (no recent activity)"
|
|
563
|
+
: phase === "stopped-clean"
|
|
564
|
+
? "fix loop · stopped — all clear"
|
|
565
|
+
: "fix loop · stopped — needs a human";
|
|
566
|
+
const ageMin = Math.max(0, Math.round(fixLoop.ageMs / 60000));
|
|
567
|
+
lines.push(ruleLine);
|
|
568
|
+
lines.push({
|
|
569
|
+
text: "",
|
|
570
|
+
segments: [
|
|
571
|
+
{ text: "⟳ ", color, bold: true },
|
|
572
|
+
{ text: label, color },
|
|
573
|
+
{ text: ` updated ${ageMin}m ago`, dim: true },
|
|
574
|
+
],
|
|
575
|
+
});
|
|
576
|
+
}
|
|
570
577
|
// ── Checks ────────────────────────────────────────────────────────
|
|
571
578
|
if (!isMain && checks && checks.length > 0) {
|
|
579
|
+
// `gh` buckets: pass | fail | pending | skipping | cancel. Skipped/cancelled
|
|
580
|
+
// checks don't run under the current conditions and don't block the PR —
|
|
581
|
+
// GitHub counts them as "skipped" while still reporting "all checks passed".
|
|
582
|
+
// Treat them as a non-blocking aside, never as pending.
|
|
572
583
|
const passing = checks.filter((c) => c.bucket === "pass");
|
|
573
584
|
const failing = checks.filter((c) => c.bucket === "fail");
|
|
574
|
-
const pending = checks.filter((c) => c.bucket
|
|
585
|
+
const pending = checks.filter((c) => c.bucket === "pending");
|
|
586
|
+
const skipped = checks.filter((c) => c.bucket === "skipping" || c.bucket === "cancel");
|
|
575
587
|
const headerColor = failing.length > 0 ? "red" : pending.length > 0 ? "yellow" : "green";
|
|
576
588
|
lines.push(ruleLine);
|
|
577
589
|
const headerSegs = [
|
|
578
590
|
{ text: "✓ ", color: "cyan", bold: true },
|
|
579
591
|
{ text: "Checks", bold: true },
|
|
580
592
|
{ text: " " },
|
|
581
|
-
{ text: `${passing.length}
|
|
593
|
+
{ text: `${passing.length} passed`, color: headerColor },
|
|
582
594
|
];
|
|
583
595
|
if (failing.length > 0) {
|
|
584
596
|
headerSegs.push({ text: " · ", dim: true });
|
|
@@ -588,8 +600,13 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
588
600
|
headerSegs.push({ text: " · ", dim: true });
|
|
589
601
|
headerSegs.push({ text: `${pending.length} pending`, color: "yellow" });
|
|
590
602
|
}
|
|
603
|
+
if (skipped.length > 0) {
|
|
604
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
605
|
+
headerSegs.push({ text: `${skipped.length} skipped`, dim: true });
|
|
606
|
+
}
|
|
591
607
|
lines.push({ text: "", segments: headerSegs });
|
|
592
608
|
// Order: failing first (most important), then pending, then passing.
|
|
609
|
+
// Skipped checks aren't listed individually — just summarized above.
|
|
593
610
|
for (const check of failing) {
|
|
594
611
|
const desc = check.description ? ` — ${check.description}` : "";
|
|
595
612
|
lines.push({ text: ` ✗ ${check.name}${desc}`, color: "red" });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FixLoopRuntime } from "../fix-loop.js";
|
|
1
2
|
import type { ProjectGroup, DashboardIssue } from "./types.js";
|
|
2
3
|
interface Props {
|
|
3
4
|
groups: ProjectGroup[];
|
|
@@ -16,6 +17,9 @@ interface Props {
|
|
|
16
17
|
/** Ticket ids whose worktree is currently being removed — shown with a
|
|
17
18
|
* distinct WT-column glyph so concurrent deletions are visible in the list. */
|
|
18
19
|
deletingIds?: Set<string>;
|
|
20
|
+
/** Active/recent auto-fix loops by ticket id — a running/stalled loop shows a
|
|
21
|
+
* ⟳ in place of the CI glyph (the loop is the CI story while it runs). */
|
|
22
|
+
fixLoops?: Record<string, FixLoopRuntime>;
|
|
19
23
|
}
|
|
20
24
|
export type ListRow = {
|
|
21
25
|
kind: "spacer";
|
|
@@ -36,5 +40,5 @@ export type ListRow = {
|
|
|
36
40
|
depth: number;
|
|
37
41
|
};
|
|
38
42
|
export declare function buildIssueListRows(groups: ProjectGroup[], flatIssues: DashboardIssue[]): ListRow[];
|
|
39
|
-
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg, variant, deletingIds, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg, variant, deletingIds, fixLoops, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
40
44
|
export {};
|