santree 0.7.5 → 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 +165 -61
- 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 +6 -1
- package/dist/commands/worktree/open.js +3 -2
- package/dist/lib/ai.d.ts +11 -0
- package/dist/lib/ai.js +71 -2
- package/dist/lib/claude-config.d.ts +18 -4
- package/dist/lib/claude-config.js +58 -23
- 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 +4 -2
- package/dist/lib/dashboard/DetailPanel.js +44 -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 +13 -2
- package/dist/lib/dashboard/types.js +33 -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 +2 -2
- package/dist/lib/setup/apply.js +2 -2
- package/dist/lib/setup/gitignore.d.ts +7 -0
- package/dist/lib/setup/gitignore.js +39 -1
- package/dist/lib/setup/steps.d.ts +6 -2
- package/dist/lib/setup/steps.js +65 -60
- package/dist/lib/setup/tools.d.ts +1 -1
- package/dist/lib/setup/tools.js +1 -1
- package/dist/lib/trackers/github/index.js +2 -2
- package/dist/lib/trackers/local/store.js +1 -1
- package/package.json +1 -1
- 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 -545
- 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/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/commands/setup.d.ts +0 -11
- package/dist/commands/setup.js +0 -194
- 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/dist/lib/setup/shell-config.d.ts +0 -38
- package/dist/lib/setup/shell-config.js +0 -131
- package/prompts/english-tutor-prompt.njk +0 -15
package/dist/commands/pr/fix.js
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages } from "../../lib/ai.js";
|
|
7
|
+
import { findMainRepoRoot, getCurrentBranch, extractTicketId } from "../../lib/git.js";
|
|
8
|
+
import { getPRInfoAsync } from "../../lib/github.js";
|
|
9
|
+
import { santreeSelfArgv } from "../../lib/setup/apply.js";
|
|
10
|
+
import { startFixLoop, signalFixLoop } from "../../lib/fix-loop.js";
|
|
11
|
+
export const description = "Run the self-driving fix loop: merge conflicts, fixable CI, and 👍-approved review comments";
|
|
12
|
+
export const options = z.object({
|
|
13
|
+
// Accepted for backward compatibility (the dashboard passes it). The fix loop
|
|
14
|
+
// is now the only flow, so this is always on.
|
|
15
|
+
loop: z.boolean().default(true).describe("Deprecated: the fix loop is always on"),
|
|
16
|
+
signal: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Internal: update this ticket's fix-loop dashboard marker, then exit"),
|
|
20
|
+
});
|
|
21
|
+
const KNOWN_STATUSES = new Set([
|
|
22
|
+
"running",
|
|
23
|
+
"merging",
|
|
24
|
+
"fixing",
|
|
25
|
+
"waiting-ci",
|
|
26
|
+
"stopped:clean",
|
|
27
|
+
"stopped:stuck",
|
|
28
|
+
]);
|
|
29
|
+
export default function Fix({ options: opts }) {
|
|
30
|
+
// --signal is a quiet internal marker update — start in its own phase so the
|
|
31
|
+
// full "Fix PR" UI never flashes before the process exits.
|
|
32
|
+
const [status, setStatus] = useState(opts.signal !== undefined ? "signaled" : "loading");
|
|
9
33
|
const [branch, setBranch] = useState(null);
|
|
10
34
|
const [ticketId, setTicketId] = useState(null);
|
|
11
35
|
const [error, setError] = useState(null);
|
|
12
36
|
useEffect(() => {
|
|
37
|
+
// --signal: lightweight marker update invoked by the loop body. Resolve
|
|
38
|
+
// repo/ticket offline (no tracker fetch) and exit fast.
|
|
39
|
+
if (opts.signal !== undefined) {
|
|
40
|
+
const mainRoot = findMainRepoRoot();
|
|
41
|
+
const br = getCurrentBranch();
|
|
42
|
+
const tid = br ? extractTicketId(br) : null;
|
|
43
|
+
if (mainRoot && tid) {
|
|
44
|
+
const s = (KNOWN_STATUSES.has(opts.signal) ? opts.signal : "running");
|
|
45
|
+
signalFixLoop(mainRoot, tid, s);
|
|
46
|
+
}
|
|
47
|
+
setStatus("signaled");
|
|
48
|
+
setTimeout(() => process.exit(0), 20);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
13
51
|
async function init() {
|
|
14
52
|
await new Promise((r) => setTimeout(r, 100));
|
|
15
53
|
setStatus("fetching");
|
|
@@ -22,18 +60,29 @@ export default function Fix() {
|
|
|
22
60
|
const ctx = result.context;
|
|
23
61
|
setBranch(ctx.branch);
|
|
24
62
|
setTicketId(ctx.ticketId);
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
63
|
+
const prInfo = await getPRInfoAsync(ctx.branch);
|
|
64
|
+
if (!prInfo) {
|
|
27
65
|
setStatus("error");
|
|
28
66
|
setError(`No pull request found for branch '${ctx.branch}'`);
|
|
29
67
|
return;
|
|
30
68
|
}
|
|
31
|
-
const diffContent = await fetchAndRenderDiff(ctx.branch);
|
|
32
69
|
setStatus("launching");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
// The loop refreshes its own context each iteration via `pr context`,
|
|
71
|
+
// so we only build the standing instructions here. Use an absolute
|
|
72
|
+
// santree invocation (the new window's shell may not have it on PATH).
|
|
73
|
+
const self = santreeSelfArgv([]);
|
|
74
|
+
const santreeCmd = [self.cmd, ...self.args].join(" ");
|
|
75
|
+
const body = renderAIPrompt("fix-loop", ctx, {
|
|
76
|
+
branch: ctx.branch,
|
|
77
|
+
santree_cmd: santreeCmd,
|
|
36
78
|
});
|
|
79
|
+
// No interval → `/loop` self-paces via ScheduleWakeup (keeps context
|
|
80
|
+
// across iterations and can genuinely stop on our conditions). Passing
|
|
81
|
+
// an interval (`/loop 5m …`) instead picks cron mode — fresh context
|
|
82
|
+
// each firing, can't self-terminate — which is wrong for a fix loop.
|
|
83
|
+
const prompt = `/loop ${body}`;
|
|
84
|
+
if (ctx.ticketId)
|
|
85
|
+
startFixLoop(ctx.mainRoot, ctx.ticketId, 5);
|
|
37
86
|
try {
|
|
38
87
|
const child = launchAgent(prompt);
|
|
39
88
|
child.on("error", (err) => {
|
|
@@ -54,5 +103,8 @@ export default function Fix() {
|
|
|
54
103
|
}
|
|
55
104
|
init();
|
|
56
105
|
}, []);
|
|
57
|
-
|
|
106
|
+
if (status === "signaled") {
|
|
107
|
+
return _jsxs(Text, { dimColor: true, children: ["fix-loop signal: ", opts.signal] });
|
|
108
|
+
}
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR \u2014 loop" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix loop " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (looping every 5 min)..." }), _jsx(Text, { dimColor: true, children: ` claude "/loop <fix-loop for ${ticketId}>"` })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
58
110
|
}
|
|
@@ -56,7 +56,12 @@ export default function Create({ options, args }) {
|
|
|
56
56
|
if (options.work) {
|
|
57
57
|
runCommand = options.plan ? "santree worktree work --plan" : "santree worktree work";
|
|
58
58
|
}
|
|
59
|
-
const result = await mux.createWindow({
|
|
59
|
+
const result = await mux.createWindow({
|
|
60
|
+
name: windowName,
|
|
61
|
+
cwd: path,
|
|
62
|
+
command: runCommand,
|
|
63
|
+
tabName: runCommand ? "work" : undefined,
|
|
64
|
+
});
|
|
60
65
|
if (!result.ok) {
|
|
61
66
|
setMessage(`Worktree created, but failed to create window${result.message ? `: ${result.message}` : ""}`);
|
|
62
67
|
setStatus("done");
|
|
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { findMainRepoRoot } from "../../lib/git.js";
|
|
6
|
+
import { getConfiguredEditor } from "../../lib/config-store.js";
|
|
6
7
|
import { execSync, spawn } from "child_process";
|
|
7
8
|
import * as fs from "fs";
|
|
8
9
|
import * as path from "path";
|
|
@@ -38,8 +39,8 @@ export default function Open({ options: opts }) {
|
|
|
38
39
|
});
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
// Resolve editor: --editor flag > SANTREE_EDITOR
|
|
42
|
-
const editor = opts.editor ||
|
|
42
|
+
// Resolve editor: --editor flag > santree config editor (or SANTREE_EDITOR) > "code"
|
|
43
|
+
const editor = opts.editor || getConfiguredEditor() || "code";
|
|
43
44
|
// Validate editor exists in PATH
|
|
44
45
|
try {
|
|
45
46
|
execSync(`which ${editor}`, { stdio: "ignore" });
|
package/dist/lib/ai.d.ts
CHANGED
|
@@ -33,6 +33,17 @@ export declare function renderAIPrompt(template: string, ctx: AIContext, extra?:
|
|
|
33
33
|
* Returns rendered markdown or null if no PR exists.
|
|
34
34
|
*/
|
|
35
35
|
export declare function fetchAndRenderPR(branch: string): Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Fetch and render the self-contained per-iteration brief the auto-fix loop
|
|
38
|
+
* consumes — state AND the exact actions to take this iteration. It covers
|
|
39
|
+
* conflict status vs. the base branch, failing checks tagged fixable/manual
|
|
40
|
+
* (logs only for the fixable ones, to keep it tight), the 👍-approved review
|
|
41
|
+
* threads, and a single computed `directive` telling the loop what to do (merge
|
|
42
|
+
* / work / wait / stop). The loop just runs this and obeys. `santreeCmd` is the
|
|
43
|
+
* absolute santree invocation embedded in the `--signal` / resolve commands.
|
|
44
|
+
* Returns null when no PR exists for the branch.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fetchAndRenderFixContext(branch: string, santreeCmd: string): Promise<string | null>;
|
|
36
47
|
/**
|
|
37
48
|
* Fetch and render diff for a branch against its base branch (async, non-blocking).
|
|
38
49
|
* Returns rendered markdown.
|
package/dist/lib/ai.js
CHANGED
|
@@ -3,9 +3,9 @@ import { existsSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir, tmpdir } from "os";
|
|
5
5
|
import { getCurrentBranch, findRepoRoot, findMainRepoRoot, getBaseBranch } from "./git.js";
|
|
6
|
-
import { renderPrompt, renderTicket, renderDiff, renderPR } from "./prompts.js";
|
|
6
|
+
import { renderPrompt, renderTicket, renderDiff, renderPR, renderFixContext, } from "./prompts.js";
|
|
7
7
|
import { getIssueTracker } from "./trackers/index.js";
|
|
8
|
-
import { getPRInfoAsync, getPRChecksAsync, getPRReviewsAsync, getPRReviewCommentsAsync, getPRConversationCommentsAsync, getFailedCheckDetailsAsync, } from "./github.js";
|
|
8
|
+
import { getPRInfoAsync, getPRChecksAsync, getPRReviewsAsync, getPRReviewCommentsAsync, getPRReviewThreadsAsync, getApprovedReviewThreads, getViewerLoginAsync, getPRConversationCommentsAsync, getFailedCheckDetailsAsync, getPRMergeStateAsync, classifyCheck, } from "./github.js";
|
|
9
9
|
import { runAsync } from "./exec.js";
|
|
10
10
|
/**
|
|
11
11
|
* Resolves repo, branch, issue identifier, and fetches the issue from the
|
|
@@ -96,6 +96,75 @@ export async function fetchAndRenderPR(branch) {
|
|
|
96
96
|
conversation_comments: conversationComments,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Fetch and render the self-contained per-iteration brief the auto-fix loop
|
|
101
|
+
* consumes — state AND the exact actions to take this iteration. It covers
|
|
102
|
+
* conflict status vs. the base branch, failing checks tagged fixable/manual
|
|
103
|
+
* (logs only for the fixable ones, to keep it tight), the 👍-approved review
|
|
104
|
+
* threads, and a single computed `directive` telling the loop what to do (merge
|
|
105
|
+
* / work / wait / stop). The loop just runs this and obeys. `santreeCmd` is the
|
|
106
|
+
* absolute santree invocation embedded in the `--signal` / resolve commands.
|
|
107
|
+
* Returns null when no PR exists for the branch.
|
|
108
|
+
*/
|
|
109
|
+
export async function fetchAndRenderFixContext(branch, santreeCmd) {
|
|
110
|
+
const prInfo = await getPRInfoAsync(branch);
|
|
111
|
+
if (!prInfo)
|
|
112
|
+
return null;
|
|
113
|
+
const baseBranch = getBaseBranch(branch);
|
|
114
|
+
const [checks, reviews, threads, mergeState, viewer] = await Promise.all([
|
|
115
|
+
getPRChecksAsync(prInfo.number),
|
|
116
|
+
getPRReviewsAsync(prInfo.number),
|
|
117
|
+
getPRReviewThreadsAsync(prInfo.number),
|
|
118
|
+
getPRMergeStateAsync(branch),
|
|
119
|
+
getViewerLoginAsync(),
|
|
120
|
+
]);
|
|
121
|
+
const failing = (checks ?? []).filter((c) => c.bucket === "fail");
|
|
122
|
+
const failedDetails = await Promise.all(failing.map((c) => getFailedCheckDetailsAsync(c)));
|
|
123
|
+
const failed_checks = failedDetails.map((d) => ({
|
|
124
|
+
...d,
|
|
125
|
+
fixable: classifyCheck(d) === "fixable",
|
|
126
|
+
}));
|
|
127
|
+
const fixable_count = failed_checks.filter((c) => c.fixable).length;
|
|
128
|
+
const manual_count = failed_checks.filter((c) => !c.fixable).length;
|
|
129
|
+
// Only genuinely-queued/running checks count as pending — `skipping`/`cancel`
|
|
130
|
+
// won't run under the current conditions, so the loop must not wait on them.
|
|
131
|
+
const pending_count = (checks ?? []).filter((c) => c.bucket === "pending").length;
|
|
132
|
+
// Only surface review comments the viewer has 👍-approved on an unresolved
|
|
133
|
+
// thread — resolved threads are skipped, and unapproved ones wait for approval.
|
|
134
|
+
const approved_comments = viewer && threads ? getApprovedReviewThreads(threads, viewer) : [];
|
|
135
|
+
const conflicts = mergeState?.hasConflicts ?? false;
|
|
136
|
+
const mergeable = mergeState?.mergeable ?? "UNKNOWN";
|
|
137
|
+
// The single recommended action, in priority order. Conflicts block CI, so
|
|
138
|
+
// resolve them first; otherwise do any fixable work; otherwise wait for CI (or
|
|
139
|
+
// for GitHub to finish computing mergeability) rather than declaring done;
|
|
140
|
+
// otherwise stop — stuck if only manual failures remain, else clean.
|
|
141
|
+
const directive = conflicts
|
|
142
|
+
? "merge"
|
|
143
|
+
: fixable_count > 0 || approved_comments.length > 0
|
|
144
|
+
? "work"
|
|
145
|
+
: pending_count > 0 || mergeable === "UNKNOWN"
|
|
146
|
+
? "wait"
|
|
147
|
+
: manual_count > 0
|
|
148
|
+
? "stop-stuck"
|
|
149
|
+
: "stop-clean";
|
|
150
|
+
return renderFixContext({
|
|
151
|
+
pr_number: prInfo.number,
|
|
152
|
+
pr_url: prInfo.url ?? "",
|
|
153
|
+
branch,
|
|
154
|
+
base_branch: baseBranch,
|
|
155
|
+
conflicts,
|
|
156
|
+
mergeable,
|
|
157
|
+
merge_state: mergeState?.mergeStateStatus ?? "UNKNOWN",
|
|
158
|
+
failed_checks,
|
|
159
|
+
fixable_count,
|
|
160
|
+
manual_count,
|
|
161
|
+
pending_count,
|
|
162
|
+
reviews,
|
|
163
|
+
approved_comments,
|
|
164
|
+
directive,
|
|
165
|
+
santree_cmd: santreeCmd,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
99
168
|
/**
|
|
100
169
|
* Fetch and render diff for a branch against its base branch (async, non-blocking).
|
|
101
170
|
* Returns rendered markdown.
|
|
@@ -4,9 +4,23 @@ export declare function readJsonSafe(filePath: string): Record<string, any>;
|
|
|
4
4
|
export declare function getStatuslineCommand(): string | undefined;
|
|
5
5
|
export declare function isStatuslineConfigured(): boolean;
|
|
6
6
|
export declare function configureStatusline(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Remove santree's statusline. Only clears `statusLine` when it's actually
|
|
9
|
+
* pointing at santree — a user's hand-rolled statusline is left untouched.
|
|
10
|
+
*/
|
|
11
|
+
export declare function removeStatusline(): string;
|
|
7
12
|
export declare function isRemoteControlEnabled(): boolean;
|
|
8
13
|
export declare function enableRemoteControl(): string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
/** Revert remote-control to the default (off) by dropping the opt-in flag. */
|
|
15
|
+
export declare function disableRemoteControl(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Strip any leftover session-signal hook entries from settings.json. The
|
|
18
|
+
* session-state feature (and its `santree helpers session-signal` command) was
|
|
19
|
+
* removed, so these hooks now invoke a command that no longer exists and fire
|
|
20
|
+
* on every Claude event. Removing them is always safe: we only touch entries
|
|
21
|
+
* whose command mentions `session-signal`, leaving every other hook on the
|
|
22
|
+
* shared events untouched. Returns the number of hook entries removed so
|
|
23
|
+
* callers can report it. Self-healing: running `santree config` once cleans up
|
|
24
|
+
* an existing install.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pruneSessionSignalHooks(): number;
|
|
@@ -6,8 +6,9 @@ import * as path from "path";
|
|
|
6
6
|
* - ~/.claude/settings.json (statusline + hooks)
|
|
7
7
|
* - ~/.claude.json (remote control)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* here so
|
|
9
|
+
* `santree config` (both the `--check` report and the interactive panel) goes
|
|
10
|
+
* through here so detection and configuration can never disagree about what
|
|
11
|
+
* "configured" means.
|
|
11
12
|
*/
|
|
12
13
|
const STATUSLINE_COMMAND = "santree helpers statusline";
|
|
13
14
|
export function claudeSettingsPath() {
|
|
@@ -48,6 +49,19 @@ export function configureStatusline() {
|
|
|
48
49
|
writeJson(settingsPath, settings);
|
|
49
50
|
return settingsPath;
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Remove santree's statusline. Only clears `statusLine` when it's actually
|
|
54
|
+
* pointing at santree — a user's hand-rolled statusline is left untouched.
|
|
55
|
+
*/
|
|
56
|
+
export function removeStatusline() {
|
|
57
|
+
const settingsPath = claudeSettingsPath();
|
|
58
|
+
if (!isStatuslineConfigured())
|
|
59
|
+
return settingsPath;
|
|
60
|
+
const settings = readJsonSafe(settingsPath);
|
|
61
|
+
delete settings.statusLine;
|
|
62
|
+
writeJson(settingsPath, settings);
|
|
63
|
+
return settingsPath;
|
|
64
|
+
}
|
|
51
65
|
// ── Remote control ────────────────────────────────────────────────────────────
|
|
52
66
|
export function isRemoteControlEnabled() {
|
|
53
67
|
return readJsonSafe(claudeConfigPath()).remoteControlAtStartup === true;
|
|
@@ -59,28 +73,49 @@ export function enableRemoteControl() {
|
|
|
59
73
|
writeJson(configPath, config);
|
|
60
74
|
return configPath;
|
|
61
75
|
}
|
|
62
|
-
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
/** Revert remote-control to the default (off) by dropping the opt-in flag. */
|
|
77
|
+
export function disableRemoteControl() {
|
|
78
|
+
const configPath = claudeConfigPath();
|
|
79
|
+
const config = readJsonSafe(configPath);
|
|
80
|
+
delete config.remoteControlAtStartup;
|
|
81
|
+
writeJson(configPath, config);
|
|
82
|
+
return configPath;
|
|
83
|
+
}
|
|
84
|
+
// ── Legacy cleanup ────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Strip any leftover session-signal hook entries from settings.json. The
|
|
87
|
+
* session-state feature (and its `santree helpers session-signal` command) was
|
|
88
|
+
* removed, so these hooks now invoke a command that no longer exists and fire
|
|
89
|
+
* on every Claude event. Removing them is always safe: we only touch entries
|
|
90
|
+
* whose command mentions `session-signal`, leaving every other hook on the
|
|
91
|
+
* shared events untouched. Returns the number of hook entries removed so
|
|
92
|
+
* callers can report it. Self-healing: running `santree config` once cleans up
|
|
93
|
+
* an existing install.
|
|
94
|
+
*/
|
|
95
|
+
export function pruneSessionSignalHooks() {
|
|
96
|
+
const settingsPath = claudeSettingsPath();
|
|
97
|
+
const settings = readJsonSafe(settingsPath);
|
|
98
|
+
const hooks = settings.hooks;
|
|
99
|
+
if (!hooks || typeof hooks !== "object")
|
|
100
|
+
return 0;
|
|
101
|
+
let removed = 0;
|
|
102
|
+
for (const event of Object.keys(hooks)) {
|
|
103
|
+
const arr = hooks[event];
|
|
104
|
+
if (!Array.isArray(arr))
|
|
73
105
|
continue;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
106
|
+
const kept = arr.filter((entry) => {
|
|
107
|
+
const inner = Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
108
|
+
const isSignal = inner.some((h) => typeof h.command === "string" && h.command.includes("session-signal"));
|
|
109
|
+
if (isSignal)
|
|
110
|
+
removed++;
|
|
111
|
+
return !isSignal;
|
|
78
112
|
});
|
|
79
|
-
if (
|
|
80
|
-
|
|
113
|
+
if (kept.length === 0)
|
|
114
|
+
delete hooks[event];
|
|
115
|
+
else
|
|
116
|
+
hooks[event] = kept;
|
|
81
117
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return missingSessionSignalHooks().length === 0;
|
|
118
|
+
if (removed > 0)
|
|
119
|
+
writeJson(settingsPath, settings);
|
|
120
|
+
return removed;
|
|
86
121
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IssueTrackerKind } from "../trackers/types.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
repoRoot: string;
|
|
4
|
+
activeKind: IssueTrackerKind | null;
|
|
5
|
+
activeOrg: string | null;
|
|
6
|
+
onApplied: (message: string) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
export default function TrackerPicker({ repoRoot, activeKind, activeOrg, onApplied, onCancel, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -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>;
|