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,36 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { resolveAIContext, fetchAndRenderFixContext } from "../../lib/ai.js";
|
|
3
|
+
import { santreeSelfArgv } from "../../lib/setup/apply.js";
|
|
4
|
+
export const description = "Print the fix-loop iteration brief (state + the exact actions to take)";
|
|
5
|
+
/**
|
|
6
|
+
* Read-only command the auto-fix `/loop` runs each iteration to refresh state.
|
|
7
|
+
* No Ink UI — it writes the rendered brief straight to stdout and exits (same
|
|
8
|
+
* pattern as the statusline helper) so the agent can read it cleanly.
|
|
9
|
+
*/
|
|
10
|
+
export default function PRContext() {
|
|
11
|
+
const hasRun = useRef(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (hasRun.current)
|
|
14
|
+
return;
|
|
15
|
+
hasRun.current = true;
|
|
16
|
+
(async () => {
|
|
17
|
+
const result = await resolveAIContext();
|
|
18
|
+
if (!result.ok) {
|
|
19
|
+
process.stderr.write(result.error + "\n");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Embed the absolute santree invocation so the brief's own `--signal` /
|
|
23
|
+
// resolve commands work in a shell that may not have santree on PATH.
|
|
24
|
+
const self = santreeSelfArgv([]);
|
|
25
|
+
const santreeCmd = [self.cmd, ...self.args].join(" ");
|
|
26
|
+
const md = await fetchAndRenderFixContext(result.context.branch, santreeCmd);
|
|
27
|
+
if (!md) {
|
|
28
|
+
process.stderr.write(`No pull request found for branch '${result.context.branch}'\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(md.trimEnd() + "\n");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
})();
|
|
34
|
+
}, []);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Run the self-driving fix loop: merge conflicts, fixable CI, and \uD83D\uDC4D-approved review comments";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
loop: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
signal: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
type Props = {
|
|
8
|
+
options: z.infer<typeof options>;
|
|
9
|
+
};
|
|
10
|
+
export default function Fix({ options: opts }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
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
|
}
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import { createWorktree, findMainRepoRoot, getDefaultBranch, pullLatest, hasInitScript, getInitScriptPath, extractTicketId, } from "../../lib/git.js";
|
|
8
8
|
import { spawnAsync } from "../../lib/exec.js";
|
|
9
|
+
import { formatCdCommand } from "../../lib/cd-hint.js";
|
|
9
10
|
import { getMultiplexer } from "../../lib/multiplexer/index.js";
|
|
10
11
|
export const description = "Create a new worktree from a branch";
|
|
11
12
|
export const options = z.object({
|
|
@@ -36,6 +37,7 @@ export default function Create({ options, args }) {
|
|
|
36
37
|
const [worktreePath, setWorktreePath] = useState("");
|
|
37
38
|
const [baseBranch, setBaseBranch] = useState(null);
|
|
38
39
|
const [muxWindowName, setMuxWindowName] = useState(null);
|
|
40
|
+
const [cdHint, setCdHint] = useState(null);
|
|
39
41
|
async function finalize(path, branch) {
|
|
40
42
|
const wantsWindow = options.window || options.tmux;
|
|
41
43
|
if (wantsWindow) {
|
|
@@ -43,7 +45,7 @@ export default function Create({ options, args }) {
|
|
|
43
45
|
if (!mux.isActive()) {
|
|
44
46
|
setMessage("Worktree created, but no active multiplexer");
|
|
45
47
|
setStatus("done");
|
|
46
|
-
|
|
48
|
+
setCdHint(formatCdCommand({ path }));
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
setStatus("spawning-window");
|
|
@@ -52,28 +54,32 @@ export default function Create({ options, args }) {
|
|
|
52
54
|
setMuxWindowName(windowName);
|
|
53
55
|
let runCommand;
|
|
54
56
|
if (options.work) {
|
|
55
|
-
runCommand = options.plan ? "
|
|
57
|
+
runCommand = options.plan ? "santree worktree work --plan" : "santree worktree work";
|
|
56
58
|
}
|
|
57
|
-
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
|
+
});
|
|
58
65
|
if (!result.ok) {
|
|
59
66
|
setMessage(`Worktree created, but failed to create window${result.message ? `: ${result.message}` : ""}`);
|
|
60
67
|
setStatus("done");
|
|
61
|
-
|
|
68
|
+
setCdHint(formatCdCommand({ path }));
|
|
62
69
|
return;
|
|
63
70
|
}
|
|
64
71
|
setStatus("done");
|
|
65
72
|
const workInfo = options.work ? (options.plan ? " + Claude (plan)" : " + Claude") : "";
|
|
66
73
|
setMessage(`Worktree and window created!${workInfo}`);
|
|
67
|
-
//
|
|
74
|
+
// No cd hint when a window is created — the user is already in the new window
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
setStatus("done");
|
|
71
78
|
setMessage("Worktree created successfully!");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
79
|
+
setCdHint(formatCdCommand({
|
|
80
|
+
path,
|
|
81
|
+
work: options.work ? { mode: options.plan ? "plan" : "implement" } : undefined,
|
|
82
|
+
}));
|
|
77
83
|
}
|
|
78
84
|
useEffect(() => {
|
|
79
85
|
async function run() {
|
|
@@ -159,5 +165,5 @@ export default function Create({ options, args }) {
|
|
|
159
165
|
status === "creating" ||
|
|
160
166
|
status === "init-script" ||
|
|
161
167
|
status === "spawning-window";
|
|
162
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83C\uDF31 Create Worktree" }) }), branchName && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : status === "done" ? "green" : "blue", paddingX: 1, width: "100%", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branchName })] }), baseBranch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "base:" }), _jsx(Text, { color: "blue", children: baseBranch })] })), options["no-pull"] && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "skip pull:" }), _jsx(Text, { color: "yellow", children: "yes" })] })), options.work && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "after:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", children: options.plan ? " plan " : " work " })] })), (options.window || options.tmux) && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "window:" }), _jsx(Text, { backgroundColor: "green", color: "white", children: ` ${options.name || "auto"} ` })] }))] })), _jsxs(Box, { marginTop: 1, children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] })), status === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] }), _jsxs(Text, { dimColor: true, children: [" ", worktreePath] }), muxWindowName && _jsxs(Text, { dimColor: true, children: [" window: ", muxWindowName] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] })] }));
|
|
168
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83C\uDF31 Create Worktree" }) }), branchName && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : status === "done" ? "green" : "blue", paddingX: 1, width: "100%", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branchName })] }), baseBranch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "base:" }), _jsx(Text, { color: "blue", children: baseBranch })] })), options["no-pull"] && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "skip pull:" }), _jsx(Text, { color: "yellow", children: "yes" })] })), options.work && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "after:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", children: options.plan ? " plan " : " work " })] })), (options.window || options.tmux) && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "window:" }), _jsx(Text, { backgroundColor: "green", color: "white", children: ` ${options.name || "auto"} ` })] }))] })), _jsxs(Box, { marginTop: 1, children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] })), status === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] }), _jsxs(Text, { dimColor: true, children: [" ", worktreePath] }), muxWindowName && _jsxs(Text, { dimColor: true, children: [" window: ", muxWindowName] }), cdHint && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "\u2192 Run this to enter the worktree:" }), _jsxs(Text, { color: "cyan", children: [" ", cdHint] })] }))] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] })] }));
|
|
163
169
|
}
|
|
@@ -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" });
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
2
|
import { Text, Box } from "ink";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { getWorktreePath } from "../../lib/git.js";
|
|
5
|
+
import { formatCdCommand } from "../../lib/cd-hint.js";
|
|
6
6
|
export const description = "Switch to another worktree";
|
|
7
7
|
export const args = z.tuple([z.string().describe("Branch name to switch to")]);
|
|
8
8
|
export default function Switch({ args }) {
|
|
9
9
|
const [branchName] = args;
|
|
10
|
-
const hasOutputRef = useRef(false);
|
|
11
10
|
// Find worktree path synchronously
|
|
12
11
|
const worktreePath = getWorktreePath(branchName);
|
|
13
|
-
// Output SANTREE_CD once (before Ink fully renders)
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (worktreePath && !hasOutputRef.current) {
|
|
16
|
-
hasOutputRef.current = true;
|
|
17
|
-
process.stdout.write(`SANTREE_CD:${worktreePath}\n`);
|
|
18
|
-
}
|
|
19
|
-
}, [worktreePath]);
|
|
20
12
|
const status = worktreePath ? "done" : "error";
|
|
21
13
|
const error = worktreePath ? null : `Worktree not found for branch: ${branchName}`;
|
|
22
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83D\uDD00 Switch" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "green", paddingX: 1, width: "100%", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branchName })] }), worktreePath && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "path:" }), _jsx(Text, { dimColor: true, children: worktreePath })] }))] }), _jsxs(Box, { marginTop: 1, children: [status === "done" && (
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83D\uDD00 Switch" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "green", paddingX: 1, width: "100%", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branchName })] }), worktreePath && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "path:" }), _jsx(Text, { dimColor: true, children: worktreePath })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [status === "done" && worktreePath && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "\u2192 Run this to enter the worktree:" }), _jsxs(Text, { color: "cyan", children: [" ", formatCdCommand({ path: worktreePath })] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
23
15
|
}
|
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.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* santree is a plain binary — a child process can't change its parent shell's
|
|
3
|
+
* working directory. So when a command wants you to land in a worktree, it
|
|
4
|
+
* prints the command for you to run. (Dashboard flows with tmux/cmux open a new
|
|
5
|
+
* window in the right directory instead and never need this.)
|
|
6
|
+
*/
|
|
7
|
+
export interface CdHint {
|
|
8
|
+
path: string;
|
|
9
|
+
/** Append a `worktree work` launch after the cd. */
|
|
10
|
+
work?: {
|
|
11
|
+
mode: "plan" | "implement" | string;
|
|
12
|
+
contextFile?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** The shell command to enter a worktree (and optionally start work). */
|
|
16
|
+
export declare function formatCdCommand(hint: CdHint): string;
|
|
17
|
+
/** Print the cd hint to stdout (for non-Ink contexts, e.g. after the dashboard exits). */
|
|
18
|
+
export declare function printCdHint(hint: CdHint): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* santree is a plain binary — a child process can't change its parent shell's
|
|
3
|
+
* working directory. So when a command wants you to land in a worktree, it
|
|
4
|
+
* prints the command for you to run. (Dashboard flows with tmux/cmux open a new
|
|
5
|
+
* window in the right directory instead and never need this.)
|
|
6
|
+
*/
|
|
7
|
+
/** Single-quote a value for safe copy-paste into a POSIX shell. */
|
|
8
|
+
function sq(s) {
|
|
9
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
10
|
+
}
|
|
11
|
+
/** The shell command to enter a worktree (and optionally start work). */
|
|
12
|
+
export function formatCdCommand(hint) {
|
|
13
|
+
let cmd = `cd ${sq(hint.path)}`;
|
|
14
|
+
if (hint.work) {
|
|
15
|
+
let work = "santree worktree work";
|
|
16
|
+
if (hint.work.mode === "plan")
|
|
17
|
+
work += " --plan";
|
|
18
|
+
if (hint.work.contextFile)
|
|
19
|
+
work += ` --context-file ${sq(hint.work.contextFile)}`;
|
|
20
|
+
cmd += ` && ${work}`;
|
|
21
|
+
}
|
|
22
|
+
return cmd;
|
|
23
|
+
}
|
|
24
|
+
/** Print the cd hint to stdout (for non-Ink contexts, e.g. after the dashboard exits). */
|
|
25
|
+
export function printCdHint(hint) {
|
|
26
|
+
console.log(`\n→ Run this to enter the worktree:\n ${formatCdCommand(hint)}`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function claudeSettingsPath(): string;
|
|
2
|
+
export declare function claudeConfigPath(): string;
|
|
3
|
+
export declare function readJsonSafe(filePath: string): Record<string, any>;
|
|
4
|
+
export declare function getStatuslineCommand(): string | undefined;
|
|
5
|
+
export declare function isStatuslineConfigured(): boolean;
|
|
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;
|
|
12
|
+
export declare function isRemoteControlEnabled(): boolean;
|
|
13
|
+
export declare function enableRemoteControl(): string;
|
|
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;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Shared read/detect/configure helpers for the two Claude Code config files
|
|
5
|
+
* santree touches:
|
|
6
|
+
* - ~/.claude/settings.json (statusline + hooks)
|
|
7
|
+
* - ~/.claude.json (remote control)
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
const STATUSLINE_COMMAND = "santree helpers statusline";
|
|
14
|
+
export function claudeSettingsPath() {
|
|
15
|
+
return path.join(process.env.HOME || "", ".claude", "settings.json");
|
|
16
|
+
}
|
|
17
|
+
export function claudeConfigPath() {
|
|
18
|
+
return path.join(process.env.HOME || "", ".claude.json");
|
|
19
|
+
}
|
|
20
|
+
export function readJsonSafe(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(filePath)) {
|
|
23
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Invalid/unreadable — caller decides whether to start fresh.
|
|
28
|
+
}
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
function writeJson(filePath, data) {
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
// ── Statusline ───────────────────────────────────────────────────────────────
|
|
36
|
+
export function getStatuslineCommand() {
|
|
37
|
+
const settings = readJsonSafe(claudeSettingsPath());
|
|
38
|
+
const cmd = settings.statusLine?.command;
|
|
39
|
+
return cmd ? String(cmd) : undefined;
|
|
40
|
+
}
|
|
41
|
+
export function isStatuslineConfigured() {
|
|
42
|
+
const cmd = getStatuslineCommand();
|
|
43
|
+
return (!!cmd && (cmd.includes("santree statusline") || cmd.includes("santree helpers statusline")));
|
|
44
|
+
}
|
|
45
|
+
export function configureStatusline() {
|
|
46
|
+
const settingsPath = claudeSettingsPath();
|
|
47
|
+
const settings = readJsonSafe(settingsPath);
|
|
48
|
+
settings.statusLine = { type: "command", command: STATUSLINE_COMMAND };
|
|
49
|
+
writeJson(settingsPath, settings);
|
|
50
|
+
return settingsPath;
|
|
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
|
+
}
|
|
65
|
+
// ── Remote control ────────────────────────────────────────────────────────────
|
|
66
|
+
export function isRemoteControlEnabled() {
|
|
67
|
+
return readJsonSafe(claudeConfigPath()).remoteControlAtStartup === true;
|
|
68
|
+
}
|
|
69
|
+
export function enableRemoteControl() {
|
|
70
|
+
const configPath = claudeConfigPath();
|
|
71
|
+
const config = readJsonSafe(configPath);
|
|
72
|
+
config.remoteControlAtStartup = true;
|
|
73
|
+
writeJson(configPath, config);
|
|
74
|
+
return configPath;
|
|
75
|
+
}
|
|
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))
|
|
105
|
+
continue;
|
|
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;
|
|
112
|
+
});
|
|
113
|
+
if (kept.length === 0)
|
|
114
|
+
delete hooks[event];
|
|
115
|
+
else
|
|
116
|
+
hooks[event] = kept;
|
|
117
|
+
}
|
|
118
|
+
if (removed > 0)
|
|
119
|
+
writeJson(settingsPath, settings);
|
|
120
|
+
return removed;
|
|
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 {};
|