santree 0.5.2 → 0.5.4
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 +145 -52
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +95 -27
- package/dist/commands/doctor.js +33 -71
- package/dist/commands/github/auth.d.ts +2 -0
- package/dist/commands/github/auth.js +56 -0
- package/dist/commands/github/index.d.ts +1 -0
- package/dist/commands/github/index.js +1 -0
- package/dist/commands/helpers/template.d.ts +1 -0
- package/dist/commands/helpers/template.js +13 -10
- package/dist/commands/issue/index.d.ts +1 -0
- package/dist/commands/issue/index.js +1 -0
- package/dist/commands/issue/open.d.ts +2 -0
- package/dist/commands/{linear → issue}/open.js +13 -11
- package/dist/commands/issue/switch.d.ts +11 -0
- package/dist/commands/issue/switch.js +38 -0
- package/dist/commands/linear/auth.js +23 -10
- package/dist/commands/linear/switch.js +7 -3
- package/dist/commands/pr/create.js +7 -5
- package/dist/commands/worktree/create.js +4 -6
- package/dist/commands/worktree/work.js +1 -1
- package/dist/lib/ai.d.ts +8 -6
- package/dist/lib/ai.js +29 -15
- package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
- package/dist/lib/dashboard/DetailPanel.js +6 -3
- package/dist/lib/dashboard/DiffOverlay.js +8 -1
- package/dist/lib/dashboard/data.js +17 -9
- package/dist/lib/dashboard/types.d.ts +3 -16
- package/dist/lib/git.d.ts +16 -33
- package/dist/lib/git.js +20 -74
- package/dist/lib/metadata.d.ts +3 -0
- package/dist/lib/metadata.js +27 -0
- package/dist/lib/multiplexer/cmux.js +1 -1
- package/dist/lib/multiplexer/types.d.ts +1 -1
- package/dist/lib/prompts.d.ts +4 -3
- package/dist/lib/prompts.js +4 -3
- package/dist/lib/session-signal.d.ts +2 -3
- package/dist/lib/session-signal.js +3 -29
- package/dist/lib/trackers/auth-store.d.ts +16 -0
- package/dist/lib/trackers/auth-store.js +57 -0
- package/dist/lib/trackers/config.d.ts +8 -0
- package/dist/lib/trackers/config.js +21 -0
- package/dist/lib/trackers/github/api.d.ts +3 -0
- package/dist/lib/trackers/github/api.js +90 -0
- package/dist/lib/trackers/github/auth.d.ts +5 -0
- package/dist/lib/trackers/github/auth.js +27 -0
- package/dist/lib/trackers/github/images.d.ts +2 -0
- package/dist/lib/trackers/github/images.js +42 -0
- package/dist/lib/trackers/github/index.d.ts +2 -0
- package/dist/lib/trackers/github/index.js +78 -0
- package/dist/lib/trackers/index.d.ts +12 -0
- package/dist/lib/trackers/index.js +34 -0
- package/dist/lib/trackers/linear/api.d.ts +4 -0
- package/dist/lib/trackers/linear/api.js +128 -0
- package/dist/lib/trackers/linear/auth.d.ts +11 -0
- package/dist/lib/trackers/linear/auth.js +206 -0
- package/dist/lib/trackers/linear/images.d.ts +2 -0
- package/dist/lib/trackers/linear/images.js +44 -0
- package/dist/lib/trackers/linear/index.d.ts +3 -0
- package/dist/lib/trackers/linear/index.js +100 -0
- package/dist/lib/trackers/types.d.ts +52 -0
- package/dist/lib/trackers/types.js +1 -0
- package/package.json +1 -1
- package/prompts/ticket.njk +3 -3
- package/dist/commands/linear/open.d.ts +0 -2
- package/dist/lib/linear.d.ts +0 -83
- package/dist/lib/linear.js +0 -482
|
@@ -36,7 +36,6 @@ export default function Create({ options, args }) {
|
|
|
36
36
|
const [worktreePath, setWorktreePath] = useState("");
|
|
37
37
|
const [baseBranch, setBaseBranch] = useState(null);
|
|
38
38
|
const [muxWindowName, setMuxWindowName] = useState(null);
|
|
39
|
-
const [muxKind, setMuxKind] = useState(null);
|
|
40
39
|
async function finalize(path, branch) {
|
|
41
40
|
const wantsWindow = options.window || options.tmux;
|
|
42
41
|
if (wantsWindow) {
|
|
@@ -48,24 +47,23 @@ export default function Create({ options, args }) {
|
|
|
48
47
|
return;
|
|
49
48
|
}
|
|
50
49
|
setStatus("spawning-window");
|
|
51
|
-
setMessage(
|
|
50
|
+
setMessage("Creating window...");
|
|
52
51
|
const windowName = getWindowName(branch, options.name);
|
|
53
52
|
setMuxWindowName(windowName);
|
|
54
|
-
setMuxKind(mux.kind);
|
|
55
53
|
let runCommand;
|
|
56
54
|
if (options.work) {
|
|
57
55
|
runCommand = options.plan ? "st worktree work --plan" : "st worktree work";
|
|
58
56
|
}
|
|
59
57
|
const result = await mux.createWindow({ name: windowName, cwd: path, command: runCommand });
|
|
60
58
|
if (!result.ok) {
|
|
61
|
-
setMessage(`Worktree created, but failed to create
|
|
59
|
+
setMessage(`Worktree created, but failed to create window${result.message ? `: ${result.message}` : ""}`);
|
|
62
60
|
setStatus("done");
|
|
63
61
|
console.log(`SANTREE_CD:${path}`);
|
|
64
62
|
return;
|
|
65
63
|
}
|
|
66
64
|
setStatus("done");
|
|
67
65
|
const workInfo = options.work ? (options.plan ? " + Claude (plan)" : " + Claude") : "";
|
|
68
|
-
setMessage(`Worktree and
|
|
66
|
+
setMessage(`Worktree and window created!${workInfo}`);
|
|
69
67
|
// Don't output SANTREE_CD when a window is created — user is already in the new window
|
|
70
68
|
return;
|
|
71
69
|
}
|
|
@@ -161,5 +159,5 @@ export default function Create({ options, args }) {
|
|
|
161
159
|
status === "creating" ||
|
|
162
160
|
status === "init-script" ||
|
|
163
161
|
status === "spawning-window";
|
|
164
|
-
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 &&
|
|
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] }))] })] }));
|
|
165
163
|
}
|
|
@@ -99,5 +99,5 @@ export default function Work({ options }) {
|
|
|
99
99
|
setError(err instanceof Error ? err.message : "Failed to launch agent");
|
|
100
100
|
}
|
|
101
101
|
}, [status, aiContext, mode]);
|
|
102
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), 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: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching
|
|
102
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), 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: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching issue from tracker..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }), _jsxs(Text, { dimColor: true, children: [" ", "claude", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
103
103
|
}
|
package/dist/lib/ai.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { type ChildProcess } from "child_process";
|
|
2
|
-
import {
|
|
2
|
+
import type { Issue } from "./trackers/types.js";
|
|
3
3
|
export interface AIContext {
|
|
4
4
|
repoRoot: string;
|
|
5
5
|
mainRoot: string;
|
|
6
6
|
branch: string;
|
|
7
7
|
ticketId: string | null;
|
|
8
|
-
ticket:
|
|
8
|
+
ticket: Issue | null;
|
|
9
|
+
trackerName: string;
|
|
10
|
+
issueNoun: string;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
|
-
* Resolves repo, branch,
|
|
12
|
-
*
|
|
13
|
+
* Resolves repo, branch, issue identifier, and fetches the issue from the
|
|
14
|
+
* active tracker (Linear or GitHub Issues — selected by repo config).
|
|
13
15
|
*/
|
|
14
16
|
export declare function resolveAIContext(): Promise<{
|
|
15
17
|
ok: true;
|
|
@@ -74,6 +76,6 @@ export declare function runAgent(prompt: string, opts?: {
|
|
|
74
76
|
allowedTools?: string[];
|
|
75
77
|
}): RunAgentResult;
|
|
76
78
|
/**
|
|
77
|
-
*
|
|
79
|
+
* Clean up cached image downloads for an issue identifier on the active tracker.
|
|
78
80
|
*/
|
|
79
|
-
export
|
|
81
|
+
export declare function cleanupImages(ticketId: string): void;
|
package/dist/lib/ai.js
CHANGED
|
@@ -2,15 +2,14 @@ import { execSync, spawn, spawnSync } from "child_process";
|
|
|
2
2
|
import { existsSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir, tmpdir } from "os";
|
|
5
|
-
import {
|
|
6
|
-
import { getCurrentBranch, extractTicketId, findRepoRoot, findMainRepoRoot, getBaseBranch, } from "./git.js";
|
|
5
|
+
import { getCurrentBranch, findRepoRoot, findMainRepoRoot, getBaseBranch } from "./git.js";
|
|
7
6
|
import { renderPrompt, renderTicket, renderDiff, renderPR } from "./prompts.js";
|
|
8
|
-
import {
|
|
7
|
+
import { getIssueTracker } from "./trackers/index.js";
|
|
9
8
|
import { getPRInfoAsync, getPRChecksAsync, getPRReviewsAsync, getPRReviewCommentsAsync, getPRConversationCommentsAsync, getFailedCheckDetailsAsync, } from "./github.js";
|
|
10
9
|
import { runAsync } from "./exec.js";
|
|
11
10
|
/**
|
|
12
|
-
* Resolves repo, branch,
|
|
13
|
-
*
|
|
11
|
+
* Resolves repo, branch, issue identifier, and fetches the issue from the
|
|
12
|
+
* active tracker (Linear or GitHub Issues — selected by repo config).
|
|
14
13
|
*/
|
|
15
14
|
export async function resolveAIContext() {
|
|
16
15
|
const repoRoot = findRepoRoot();
|
|
@@ -21,18 +20,28 @@ export async function resolveAIContext() {
|
|
|
21
20
|
if (!branch) {
|
|
22
21
|
return { ok: false, error: "Could not determine current branch" };
|
|
23
22
|
}
|
|
24
|
-
const
|
|
23
|
+
const mainRoot = findMainRepoRoot() ?? repoRoot;
|
|
24
|
+
const tracker = getIssueTracker(mainRoot);
|
|
25
|
+
const ticketId = tracker.extractIdFromBranch(branch);
|
|
25
26
|
if (!ticketId) {
|
|
26
27
|
return {
|
|
27
28
|
ok: false,
|
|
28
|
-
error:
|
|
29
|
+
error: `Could not extract ${tracker.issueNoun} ID from branch name '${branch}'.`,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
|
-
const
|
|
32
|
-
const ticket =
|
|
32
|
+
const result = await tracker.getIssue(ticketId, mainRoot);
|
|
33
|
+
const ticket = result.ok ? result.value : null;
|
|
33
34
|
return {
|
|
34
35
|
ok: true,
|
|
35
|
-
context: {
|
|
36
|
+
context: {
|
|
37
|
+
repoRoot,
|
|
38
|
+
mainRoot,
|
|
39
|
+
branch,
|
|
40
|
+
ticketId,
|
|
41
|
+
ticket,
|
|
42
|
+
trackerName: tracker.displayName,
|
|
43
|
+
issueNoun: tracker.issueNoun,
|
|
44
|
+
},
|
|
36
45
|
};
|
|
37
46
|
}
|
|
38
47
|
/**
|
|
@@ -41,7 +50,7 @@ export async function resolveAIContext() {
|
|
|
41
50
|
export function buildPromptContext(ctx, extra) {
|
|
42
51
|
return {
|
|
43
52
|
ticket_id: ctx.ticketId ?? undefined,
|
|
44
|
-
ticket_content: ctx.ticket ? renderTicket(ctx.ticket) : undefined,
|
|
53
|
+
ticket_content: ctx.ticket ? renderTicket(ctx.ticket, ctx.trackerName) : undefined,
|
|
45
54
|
...extra,
|
|
46
55
|
};
|
|
47
56
|
}
|
|
@@ -122,8 +131,10 @@ const CMUX_CLAUDE_PATH = "/Applications/cmux.app/Contents/Resources/bin/claude";
|
|
|
122
131
|
*/
|
|
123
132
|
export function resolveClaudeBinary() {
|
|
124
133
|
// Inside cmux, the bundled binary is the only one wired to the active
|
|
125
|
-
// workspace.
|
|
126
|
-
|
|
134
|
+
// workspace. Gate on `CMUX_SURFACE_ID` (actual cmux runtime), not
|
|
135
|
+
// `SANTREE_MULTIPLEXER=cmux` — outside a live workspace the bundled
|
|
136
|
+
// binary has no auth context and exits with "Invalid API key".
|
|
137
|
+
if (process.env["CMUX_SURFACE_ID"] && existsSync(CMUX_CLAUDE_PATH)) {
|
|
127
138
|
return CMUX_CLAUDE_PATH;
|
|
128
139
|
}
|
|
129
140
|
// PATH lookup
|
|
@@ -216,6 +227,9 @@ export function runAgent(prompt, opts) {
|
|
|
216
227
|
};
|
|
217
228
|
}
|
|
218
229
|
/**
|
|
219
|
-
*
|
|
230
|
+
* Clean up cached image downloads for an issue identifier on the active tracker.
|
|
220
231
|
*/
|
|
221
|
-
export
|
|
232
|
+
export function cleanupImages(ticketId) {
|
|
233
|
+
const repoRoot = findMainRepoRoot();
|
|
234
|
+
getIssueTracker(repoRoot).cleanupCache(ticketId);
|
|
235
|
+
}
|
|
@@ -14,7 +14,10 @@ export type IssueActionItem = {
|
|
|
14
14
|
};
|
|
15
15
|
/** Returns the context-sensitive action key list for the selected issue.
|
|
16
16
|
* Lifted out of the panel so the dashboard can render it on the same row as
|
|
17
|
-
* the global command bar (so left- and right-pane key hints align).
|
|
18
|
-
|
|
17
|
+
* the global command bar (so left- and right-pane key hints align). The
|
|
18
|
+
* `trackerName` is the active tracker's `displayName` ("Linear" / "GitHub"),
|
|
19
|
+
* surfaced as the open-in-browser action label so the panel doesn't hardcode
|
|
20
|
+
* a vendor name. */
|
|
21
|
+
export declare function buildIssueActions(di: DashboardIssue, trackerName: string): IssueActionItem[];
|
|
19
22
|
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
20
23
|
export {};
|
|
@@ -51,8 +51,11 @@ function fileColor(xy) {
|
|
|
51
51
|
}
|
|
52
52
|
/** Returns the context-sensitive action key list for the selected issue.
|
|
53
53
|
* Lifted out of the panel so the dashboard can render it on the same row as
|
|
54
|
-
* the global command bar (so left- and right-pane key hints align).
|
|
55
|
-
|
|
54
|
+
* the global command bar (so left- and right-pane key hints align). The
|
|
55
|
+
* `trackerName` is the active tracker's `displayName` ("Linear" / "GitHub"),
|
|
56
|
+
* surfaced as the open-in-browser action label so the panel doesn't hardcode
|
|
57
|
+
* a vendor name. */
|
|
58
|
+
export function buildIssueActions(di, trackerName) {
|
|
56
59
|
const { worktree, pr, issue } = di;
|
|
57
60
|
const items = [];
|
|
58
61
|
if (worktree?.sessionId) {
|
|
@@ -82,7 +85,7 @@ export function buildIssueActions(di) {
|
|
|
82
85
|
items.push({ key: "r", label: "Review", color: "cyan" });
|
|
83
86
|
}
|
|
84
87
|
if (issue.url) {
|
|
85
|
-
items.push({ key: "o", label:
|
|
88
|
+
items.push({ key: "o", label: trackerName, color: "gray" });
|
|
86
89
|
}
|
|
87
90
|
if (pr)
|
|
88
91
|
items.push({ key: "p", label: "Open PR", color: "gray" });
|
|
@@ -294,6 +294,13 @@ export default function DiffOverlay({ width, height, ticketId, baseBranch, files
|
|
|
294
294
|
// rightWidth includes the paddingLeft={1} of the wrapper Box,
|
|
295
295
|
// so usable column count is rightWidth - 1.
|
|
296
296
|
const cell = truncateVisible(line.text || " ", Math.max(1, rightWidth - 1));
|
|
297
|
-
|
|
297
|
+
// wrap="truncate" prevents Ink from soft-wrapping. Default
|
|
298
|
+
// `wrap` mode measures byte length (counting ANSI escape
|
|
299
|
+
// bytes as visible chars), which makes color-heavy lines
|
|
300
|
+
// like syntax-highlighted code wrap *very* early — visible
|
|
301
|
+
// content gets clobbered by the next row. truncateVisible
|
|
302
|
+
// has already sized the cell, so `truncate` is a no-op for
|
|
303
|
+
// already-fitting lines.
|
|
304
|
+
return (_jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, wrap: "truncate", children: cell }, i));
|
|
298
305
|
})) })] }))] }));
|
|
299
306
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { listWorktrees, extractTicketId, getBaseBranch, readAllMetadata, readSessionState, isSessionAlive, clearSessionState, getGitStatusAsync, getCommitsAheadAsync, getDiffShortstatAsync, } from "../git.js";
|
|
2
2
|
import { getPRInfoAsync, getPRChecksAsync, getPRReviewsAsync, getPRConversationCommentsAsync, getPRViewAsync, getReviewRequestedPRsAsync, getRepoNameAsync, } from "../github.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getIssueTracker } from "../trackers/index.js";
|
|
4
4
|
export async function loadDashboardData(repoRoot) {
|
|
5
5
|
// Fetch issues and worktrees in parallel
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const tracker = getIssueTracker(repoRoot);
|
|
7
|
+
const [listResult, worktrees] = await Promise.all([
|
|
8
|
+
tracker.listAssigned(repoRoot),
|
|
8
9
|
Promise.resolve(listWorktrees()),
|
|
9
10
|
]);
|
|
10
|
-
if (!
|
|
11
|
-
|
|
11
|
+
if (!listResult.ok) {
|
|
12
|
+
const status = await tracker.getAuthStatus(repoRoot);
|
|
13
|
+
throw new Error(listResult.message ?? status.hint ?? `Failed to authenticate with ${tracker.displayName}`);
|
|
14
|
+
}
|
|
15
|
+
const issues = listResult.value;
|
|
12
16
|
// Build worktree map: ticketId -> worktree info
|
|
13
17
|
const wtMap = new Map();
|
|
14
18
|
for (const wt of worktrees) {
|
|
@@ -92,10 +96,14 @@ export async function loadDashboardData(repoRoot) {
|
|
|
92
96
|
getPRReviewsAsync(pr.number),
|
|
93
97
|
]);
|
|
94
98
|
}
|
|
95
|
-
// Derive a readable title from branch name: strip prefix and
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
// Derive a readable title from branch name: strip prefix and the
|
|
100
|
+
// tracker-format ID literal (e.g. "TEAM-123-" or "123-"). The ID
|
|
101
|
+
// shape comes from the tracker's parser so this works for both
|
|
102
|
+
// Linear and GitHub branches.
|
|
103
|
+
const idLiteral = tid ? new RegExp(`^${tid}-?`) : null;
|
|
104
|
+
const titleFromBranch = (idLiteral
|
|
105
|
+
? wt.branch.replace(/^[^/]+\//, "").replace(idLiteral, "")
|
|
106
|
+
: wt.branch.replace(/^[^/]+\//, ""))
|
|
99
107
|
.replace(/-/g, " ")
|
|
100
108
|
.trim() || tid;
|
|
101
109
|
let sessState = readSessionState(repoRoot, tid);
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import type { PRInfo, PRCheck, PRReview, PRConversationComment, SearchPR } from "../github.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
title: string;
|
|
5
|
-
description: string | null;
|
|
6
|
-
url: string;
|
|
7
|
-
priority: number;
|
|
8
|
-
priorityLabel: string;
|
|
9
|
-
state: {
|
|
10
|
-
name: string;
|
|
11
|
-
type: string;
|
|
12
|
-
};
|
|
13
|
-
labels: string[];
|
|
14
|
-
projectId: string | null;
|
|
15
|
-
projectName: string | null;
|
|
16
|
-
}
|
|
2
|
+
import type { AssignedIssue } from "../trackers/types.js";
|
|
3
|
+
export type { AssignedIssue } from "../trackers/types.js";
|
|
17
4
|
export interface WorktreeInfo {
|
|
18
5
|
path: string;
|
|
19
6
|
branch: string;
|
|
@@ -30,7 +17,7 @@ export interface WorktreeInfo {
|
|
|
30
17
|
} | null;
|
|
31
18
|
}
|
|
32
19
|
export interface DashboardIssue {
|
|
33
|
-
issue:
|
|
20
|
+
issue: AssignedIssue;
|
|
34
21
|
worktree: WorktreeInfo | null;
|
|
35
22
|
pr: PRInfo | null;
|
|
36
23
|
checks: PRCheck[] | null;
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
1
2
|
export interface SessionState {
|
|
2
3
|
state: "waiting" | "idle" | "active" | "exited";
|
|
3
4
|
message: string | null;
|
|
@@ -53,10 +54,6 @@ export declare function getDefaultBranch(): string;
|
|
|
53
54
|
* Returns an empty array on failure.
|
|
54
55
|
*/
|
|
55
56
|
export declare function listWorktrees(): Worktree[];
|
|
56
|
-
/**
|
|
57
|
-
* Get the path to the .santree directory inside a repo root.
|
|
58
|
-
*/
|
|
59
|
-
export declare function getSantreeDir(repoRoot: string): string;
|
|
60
57
|
/**
|
|
61
58
|
* Get the path to the .santree/worktrees directory inside a repo root.
|
|
62
59
|
*/
|
|
@@ -82,9 +79,14 @@ export declare function removeWorktree(branchName: string, repoRoot: string, for
|
|
|
82
79
|
error?: string;
|
|
83
80
|
}>;
|
|
84
81
|
/**
|
|
85
|
-
* Extract
|
|
86
|
-
*
|
|
87
|
-
*
|
|
82
|
+
* Extract an issue identifier from a branch name. Delegates to the active
|
|
83
|
+
* issue tracker (Linear: `[A-Z]+-\d+`; GitHub: explicit-prefix numeric),
|
|
84
|
+
* resolving the tracker from the main repo's config.
|
|
85
|
+
*
|
|
86
|
+
* This is the single shim that frees every caller from having to know which
|
|
87
|
+
* tracker is active or how it formats IDs. Returns null when the active
|
|
88
|
+
* tracker doesn't recognize the branch's ID shape, or when no main repo can
|
|
89
|
+
* be resolved (e.g. before a worktree is created).
|
|
88
90
|
*/
|
|
89
91
|
export declare function extractTicketId(branch: string): string | null;
|
|
90
92
|
/**
|
|
@@ -93,30 +95,6 @@ export declare function extractTicketId(branch: string): string | null;
|
|
|
93
95
|
* Returns null if no worktree is checked out on that branch.
|
|
94
96
|
*/
|
|
95
97
|
export declare function getWorktreePath(branchName: string): string | null;
|
|
96
|
-
/**
|
|
97
|
-
* Read all entries from .santree/metadata.json.
|
|
98
|
-
* Returns an empty object if the file doesn't exist or can't be parsed.
|
|
99
|
-
*/
|
|
100
|
-
export declare function readAllMetadata(repoRoot: string): Record<string, any>;
|
|
101
|
-
/**
|
|
102
|
-
* Write all entries to .santree/metadata.json.
|
|
103
|
-
*/
|
|
104
|
-
export declare function writeAllMetadata(repoRoot: string, data: Record<string, any>): void;
|
|
105
|
-
/**
|
|
106
|
-
* Get the Linear org slug associated with this repo.
|
|
107
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
108
|
-
*/
|
|
109
|
-
export declare function getRepoLinearOrg(repoRoot: string): string | null;
|
|
110
|
-
/**
|
|
111
|
-
* Associate a Linear org slug with this repo.
|
|
112
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
113
|
-
*/
|
|
114
|
-
export declare function setRepoLinearOrg(repoRoot: string, orgSlug: string): void;
|
|
115
|
-
/**
|
|
116
|
-
* Remove the Linear org association from this repo.
|
|
117
|
-
* Deletes the `_linear` key from .santree/metadata.json.
|
|
118
|
-
*/
|
|
119
|
-
export declare function removeRepoLinearOrg(repoRoot: string): void;
|
|
120
98
|
/**
|
|
121
99
|
* Get the stored session ID for a given ticket from .santree/metadata.json.
|
|
122
100
|
* Returns null if no session ID is stored.
|
|
@@ -191,8 +169,13 @@ export declare function getCommitsAhead(baseBranch: string): number;
|
|
|
191
169
|
*/
|
|
192
170
|
export declare function getCommitsAheadAsync(cwd: string, baseBranch: string): Promise<number>;
|
|
193
171
|
/**
|
|
194
|
-
* Read the SANTREE_DIFF_TOOL env var, returning the configured pager
|
|
195
|
-
* (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
172
|
+
* Read the SANTREE_DIFF_TOOL env var, returning the configured diff pager
|
|
173
|
+
* command (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
174
|
+
*
|
|
175
|
+
* The CLI `worktree diff` flow lets the pager do its full job (render +
|
|
176
|
+
* scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
|
|
177
|
+
* the rendering half — it captures the pager's stdout as a string and
|
|
178
|
+
* handles scrolling itself in Ink.
|
|
196
179
|
*
|
|
197
180
|
* The value is restricted to a safe shell-token character set since it ends
|
|
198
181
|
* up in arguments passed to spawn() — even though we never use shell:true,
|
package/dist/lib/git.js
CHANGED
|
@@ -4,6 +4,9 @@ import * as path from "path";
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import { run, runAsync, spawnAsync } from "./exec.js";
|
|
6
6
|
import { getMultiplexer } from "./multiplexer/index.js";
|
|
7
|
+
import { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
8
|
+
import { getIssueTracker } from "./trackers/index.js";
|
|
9
|
+
export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
7
10
|
const execAsync = promisify(exec);
|
|
8
11
|
/**
|
|
9
12
|
* Find the toplevel directory of the current git repository.
|
|
@@ -114,12 +117,6 @@ export function listWorktrees() {
|
|
|
114
117
|
}
|
|
115
118
|
return worktrees;
|
|
116
119
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Get the path to the .santree directory inside a repo root.
|
|
119
|
-
*/
|
|
120
|
-
export function getSantreeDir(repoRoot) {
|
|
121
|
-
return path.join(repoRoot, ".santree");
|
|
122
|
-
}
|
|
123
120
|
/**
|
|
124
121
|
* Get the path to the .santree/worktrees directory inside a repo root.
|
|
125
122
|
*/
|
|
@@ -247,16 +244,18 @@ export async function removeWorktree(branchName, repoRoot, force = false) {
|
|
|
247
244
|
}
|
|
248
245
|
}
|
|
249
246
|
/**
|
|
250
|
-
* Extract
|
|
251
|
-
*
|
|
252
|
-
*
|
|
247
|
+
* Extract an issue identifier from a branch name. Delegates to the active
|
|
248
|
+
* issue tracker (Linear: `[A-Z]+-\d+`; GitHub: explicit-prefix numeric),
|
|
249
|
+
* resolving the tracker from the main repo's config.
|
|
250
|
+
*
|
|
251
|
+
* This is the single shim that frees every caller from having to know which
|
|
252
|
+
* tracker is active or how it formats IDs. Returns null when the active
|
|
253
|
+
* tracker doesn't recognize the branch's ID shape, or when no main repo can
|
|
254
|
+
* be resolved (e.g. before a worktree is created).
|
|
253
255
|
*/
|
|
254
256
|
export function extractTicketId(branch) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
return `${match[1].toUpperCase()}-${match[2]}`;
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
257
|
+
const repoRoot = findMainRepoRoot();
|
|
258
|
+
return getIssueTracker(repoRoot).extractIdFromBranch(branch);
|
|
260
259
|
}
|
|
261
260
|
/**
|
|
262
261
|
* Get the filesystem path for a worktree by its branch name.
|
|
@@ -278,64 +277,6 @@ export function getWorktreePath(branchName) {
|
|
|
278
277
|
}
|
|
279
278
|
return null;
|
|
280
279
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Get path to centralized metadata file: .santree/metadata.json in the repo root.
|
|
283
|
-
*/
|
|
284
|
-
function getMetadataFilePath(repoRoot) {
|
|
285
|
-
return path.join(getSantreeDir(repoRoot), "metadata.json");
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Read all entries from .santree/metadata.json.
|
|
289
|
-
* Returns an empty object if the file doesn't exist or can't be parsed.
|
|
290
|
-
*/
|
|
291
|
-
export function readAllMetadata(repoRoot) {
|
|
292
|
-
const filePath = getMetadataFilePath(repoRoot);
|
|
293
|
-
if (!fs.existsSync(filePath))
|
|
294
|
-
return {};
|
|
295
|
-
try {
|
|
296
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Write all entries to .santree/metadata.json.
|
|
304
|
-
*/
|
|
305
|
-
export function writeAllMetadata(repoRoot, data) {
|
|
306
|
-
const filePath = getMetadataFilePath(repoRoot);
|
|
307
|
-
const dir = path.dirname(filePath);
|
|
308
|
-
if (!fs.existsSync(dir)) {
|
|
309
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
310
|
-
}
|
|
311
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Get the Linear org slug associated with this repo.
|
|
315
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
316
|
-
*/
|
|
317
|
-
export function getRepoLinearOrg(repoRoot) {
|
|
318
|
-
const all = readAllMetadata(repoRoot);
|
|
319
|
-
return all._linear?.org ?? null;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Associate a Linear org slug with this repo.
|
|
323
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
324
|
-
*/
|
|
325
|
-
export function setRepoLinearOrg(repoRoot, orgSlug) {
|
|
326
|
-
const all = readAllMetadata(repoRoot);
|
|
327
|
-
all._linear = { org: orgSlug };
|
|
328
|
-
writeAllMetadata(repoRoot, all);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Remove the Linear org association from this repo.
|
|
332
|
-
* Deletes the `_linear` key from .santree/metadata.json.
|
|
333
|
-
*/
|
|
334
|
-
export function removeRepoLinearOrg(repoRoot) {
|
|
335
|
-
const all = readAllMetadata(repoRoot);
|
|
336
|
-
delete all._linear;
|
|
337
|
-
writeAllMetadata(repoRoot, all);
|
|
338
|
-
}
|
|
339
280
|
/**
|
|
340
281
|
* Get the stored session ID for a given ticket from .santree/metadata.json.
|
|
341
282
|
* Returns null if no session ID is stored.
|
|
@@ -469,8 +410,13 @@ export async function getCommitsAheadAsync(cwd, baseBranch) {
|
|
|
469
410
|
return output ? parseInt(output, 10) || 0 : 0;
|
|
470
411
|
}
|
|
471
412
|
/**
|
|
472
|
-
* Read the SANTREE_DIFF_TOOL env var, returning the configured pager
|
|
473
|
-
* (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
413
|
+
* Read the SANTREE_DIFF_TOOL env var, returning the configured diff pager
|
|
414
|
+
* command (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
415
|
+
*
|
|
416
|
+
* The CLI `worktree diff` flow lets the pager do its full job (render +
|
|
417
|
+
* scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
|
|
418
|
+
* the rendering half — it captures the pager's stdout as a string and
|
|
419
|
+
* handles scrolling itself in Ink.
|
|
474
420
|
*
|
|
475
421
|
* The value is restricted to a safe shell-token character set since it ends
|
|
476
422
|
* up in arguments passed to spawn() — even though we never use shell:true,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
export function getSantreeDir(repoRoot) {
|
|
4
|
+
return path.join(repoRoot, ".santree");
|
|
5
|
+
}
|
|
6
|
+
function getMetadataFilePath(repoRoot) {
|
|
7
|
+
return path.join(getSantreeDir(repoRoot), "metadata.json");
|
|
8
|
+
}
|
|
9
|
+
export function readAllMetadata(repoRoot) {
|
|
10
|
+
const filePath = getMetadataFilePath(repoRoot);
|
|
11
|
+
if (!fs.existsSync(filePath))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function writeAllMetadata(repoRoot, data) {
|
|
21
|
+
const filePath = getMetadataFilePath(repoRoot);
|
|
22
|
+
const dir = path.dirname(filePath);
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
27
|
+
}
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Issue } from "./trackers/types.js";
|
|
2
2
|
import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversationComment } from "./github.js";
|
|
3
3
|
/**
|
|
4
4
|
* Render a nunjucks template from the prompts/ directory.
|
|
@@ -7,9 +7,10 @@ import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversat
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function renderPrompt(template: string, context: Record<string, string | undefined>): string;
|
|
9
9
|
/**
|
|
10
|
-
* Render
|
|
10
|
+
* Render an issue into formatted markdown using the ticket template.
|
|
11
|
+
* `trackerName` is injected for header/link text ("Linear" / "GitHub").
|
|
11
12
|
*/
|
|
12
|
-
export declare function renderTicket(issue:
|
|
13
|
+
export declare function renderTicket(issue: Issue, trackerName: string): string;
|
|
13
14
|
export interface DiffData {
|
|
14
15
|
base_branch: string;
|
|
15
16
|
commit_log: string | null;
|
package/dist/lib/prompts.js
CHANGED
|
@@ -33,10 +33,11 @@ export function renderPrompt(template, context) {
|
|
|
33
33
|
return promptsEnv.render(`${template}.njk`, context);
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Render
|
|
36
|
+
* Render an issue into formatted markdown using the ticket template.
|
|
37
|
+
* `trackerName` is injected for header/link text ("Linear" / "GitHub").
|
|
37
38
|
*/
|
|
38
|
-
export function renderTicket(issue) {
|
|
39
|
-
return promptsEnv.render("ticket.njk", issue);
|
|
39
|
+
export function renderTicket(issue, trackerName) {
|
|
40
|
+
return promptsEnv.render("ticket.njk", { ...issue, trackerName });
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* Render diff data into formatted markdown using the diff template.
|
|
@@ -4,11 +4,10 @@ export declare function extractRepoAndTicket(cwd: string): {
|
|
|
4
4
|
repoRoot: string;
|
|
5
5
|
ticketId: string;
|
|
6
6
|
} | null;
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function runHookScript(repoRoot: string, state: SessionStateValue, env: Record<string, string>): void;
|
|
7
|
+
export declare function renameSessionWindow(ticketId: string, state: SessionStateValue): void;
|
|
9
8
|
/**
|
|
10
9
|
* Unified helper: reads stdin, extracts repo/ticket, writes state file,
|
|
11
|
-
* renames
|
|
10
|
+
* renames the multiplexer window, then exits.
|
|
12
11
|
*/
|
|
13
12
|
export declare function signalState(state: SessionStateValue): void;
|
|
14
13
|
export declare function getHooksJson(): Record<string, unknown>;
|