santree 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/dist/commands/dashboard.js +465 -97
- package/dist/commands/doctor.js +103 -17
- package/dist/commands/helpers/statusline.js +10 -2
- package/dist/commands/update.d.ts +15 -0
- package/dist/commands/update.js +72 -0
- package/dist/commands/worktree/diff.d.ts +13 -0
- package/dist/commands/worktree/diff.js +76 -0
- package/dist/lib/ai.d.ts +12 -2
- package/dist/lib/ai.js +37 -6
- package/dist/lib/dashboard/DetailPanel.d.ts +9 -0
- package/dist/lib/dashboard/DetailPanel.js +235 -89
- package/dist/lib/dashboard/DiffOverlay.d.ts +61 -0
- package/dist/lib/dashboard/DiffOverlay.js +262 -0
- package/dist/lib/dashboard/IssueList.d.ts +20 -3
- package/dist/lib/dashboard/IssueList.js +74 -103
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +6 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +4 -7
- package/dist/lib/dashboard/ReviewList.d.ts +3 -1
- package/dist/lib/dashboard/ReviewList.js +7 -7
- package/dist/lib/dashboard/data.js +10 -4
- package/dist/lib/dashboard/theme.d.ts +24 -0
- package/dist/lib/dashboard/theme.js +113 -0
- package/dist/lib/dashboard/types.d.ts +52 -1
- package/dist/lib/dashboard/types.js +81 -0
- package/dist/lib/git.d.ts +20 -0
- package/dist/lib/git.js +37 -0
- package/dist/lib/version.d.ts +55 -0
- package/dist/lib/version.js +224 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listWorktrees, extractTicketId, getBaseBranch, readAllMetadata, readSessionState, isSessionAlive, clearSessionState, getGitStatusAsync, getCommitsAheadAsync, } from "../git.js";
|
|
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
3
|
import { fetchAssignedIssues } from "../linear.js";
|
|
4
4
|
export async function loadDashboardData(repoRoot) {
|
|
@@ -33,10 +33,11 @@ export async function loadDashboardData(repoRoot) {
|
|
|
33
33
|
let reviewsInfo = null;
|
|
34
34
|
if (wt) {
|
|
35
35
|
const base = getBaseBranch(wt.branch);
|
|
36
|
-
const [gitStatusOutput, ahead, pr] = await Promise.all([
|
|
36
|
+
const [gitStatusOutput, ahead, pr, shortstat] = await Promise.all([
|
|
37
37
|
getGitStatusAsync(wt.path),
|
|
38
38
|
getCommitsAheadAsync(wt.path, base),
|
|
39
39
|
getPRInfoAsync(wt.branch),
|
|
40
|
+
getDiffShortstatAsync(wt.path, base),
|
|
40
41
|
]);
|
|
41
42
|
let sessState = readSessionState(repoRoot, issue.identifier);
|
|
42
43
|
// Validate against the active multiplexer — if the session has gone, clear stale state
|
|
@@ -54,6 +55,7 @@ export async function loadDashboardData(repoRoot) {
|
|
|
54
55
|
gitStatus: gitStatusOutput,
|
|
55
56
|
sessionState: ss === "exited" ? null : ss,
|
|
56
57
|
sessionMessage: sessState?.message ?? null,
|
|
58
|
+
diffStats: shortstat,
|
|
57
59
|
};
|
|
58
60
|
prInfo = pr;
|
|
59
61
|
if (pr) {
|
|
@@ -76,10 +78,11 @@ export async function loadDashboardData(repoRoot) {
|
|
|
76
78
|
.filter(([tid]) => !consumedTicketIds.has(tid))
|
|
77
79
|
.map(async ([tid, wt]) => {
|
|
78
80
|
const base = getBaseBranch(wt.branch);
|
|
79
|
-
const [gitStatusOutput, ahead, pr] = await Promise.all([
|
|
81
|
+
const [gitStatusOutput, ahead, pr, shortstat] = await Promise.all([
|
|
80
82
|
getGitStatusAsync(wt.path),
|
|
81
83
|
getCommitsAheadAsync(wt.path, base),
|
|
82
84
|
getPRInfoAsync(wt.branch),
|
|
85
|
+
getDiffShortstatAsync(wt.path, base),
|
|
83
86
|
]);
|
|
84
87
|
let checksInfo = null;
|
|
85
88
|
let reviewsInfo = null;
|
|
@@ -123,6 +126,7 @@ export async function loadDashboardData(repoRoot) {
|
|
|
123
126
|
gitStatus: gitStatusOutput,
|
|
124
127
|
sessionState: ss === "exited" ? null : ss,
|
|
125
128
|
sessionMessage: sessState?.message ?? null,
|
|
129
|
+
diffStats: shortstat,
|
|
126
130
|
},
|
|
127
131
|
pr,
|
|
128
132
|
checks: checksInfo,
|
|
@@ -255,9 +259,10 @@ export async function loadReviewsData(repoRoot) {
|
|
|
255
259
|
if (wt) {
|
|
256
260
|
const ticketId = extractTicketId(branch);
|
|
257
261
|
const base = getBaseBranch(branch);
|
|
258
|
-
const [gitStatusOutput, ahead] = await Promise.all([
|
|
262
|
+
const [gitStatusOutput, ahead, shortstat] = await Promise.all([
|
|
259
263
|
getGitStatusAsync(wt.path),
|
|
260
264
|
getCommitsAheadAsync(wt.path, base),
|
|
265
|
+
getDiffShortstatAsync(wt.path, base),
|
|
261
266
|
]);
|
|
262
267
|
let sessState = ticketId ? readSessionState(repoRoot, ticketId) : null;
|
|
263
268
|
if (sessState && ticketId && !isSessionAlive(ticketId)) {
|
|
@@ -274,6 +279,7 @@ export async function loadReviewsData(repoRoot) {
|
|
|
274
279
|
gitStatus: gitStatusOutput,
|
|
275
280
|
sessionState: ss === "exited" ? null : ss,
|
|
276
281
|
sessionMessage: sessState?.message ?? null,
|
|
282
|
+
diffStats: shortstat,
|
|
277
283
|
};
|
|
278
284
|
}
|
|
279
285
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard theme detection — picks light vs dark based on the terminal's
|
|
3
|
+
* actual background color (queried via OSC 11) or an explicit override from
|
|
4
|
+
* SANTREE_THEME (`light` / `dark` / `auto`, default `auto`).
|
|
5
|
+
*
|
|
6
|
+
* Most foreground colors used in the dashboard are terminal-native names
|
|
7
|
+
* (`green`, `red`, `yellow`, `cyan`, etc.) that the terminal renders in a
|
|
8
|
+
* scheme-appropriate way, so the only piece of styling that needs to flip
|
|
9
|
+
* is the selection background. That's the surface this module exposes.
|
|
10
|
+
*/
|
|
11
|
+
export type ThemeMode = "light" | "dark";
|
|
12
|
+
export interface DashboardTheme {
|
|
13
|
+
mode: ThemeMode;
|
|
14
|
+
selectionBg: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function getThemeForMode(mode: ThemeMode): DashboardTheme;
|
|
17
|
+
/**
|
|
18
|
+
* Query the terminal for its background color via OSC 11 and resolve the
|
|
19
|
+
* detected ThemeMode within `timeoutMs`. Falls back to `dark` on timeout or
|
|
20
|
+
* non-TTY stdin/stdout.
|
|
21
|
+
*
|
|
22
|
+
* The returned promise never rejects — failures resolve to `dark`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectTerminalTheme(timeoutMs?: number): Promise<ThemeMode>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard theme detection — picks light vs dark based on the terminal's
|
|
3
|
+
* actual background color (queried via OSC 11) or an explicit override from
|
|
4
|
+
* SANTREE_THEME (`light` / `dark` / `auto`, default `auto`).
|
|
5
|
+
*
|
|
6
|
+
* Most foreground colors used in the dashboard are terminal-native names
|
|
7
|
+
* (`green`, `red`, `yellow`, `cyan`, etc.) that the terminal renders in a
|
|
8
|
+
* scheme-appropriate way, so the only piece of styling that needs to flip
|
|
9
|
+
* is the selection background. That's the surface this module exposes.
|
|
10
|
+
*/
|
|
11
|
+
const DARK = { mode: "dark", selectionBg: "#1e3a5f" };
|
|
12
|
+
const LIGHT = { mode: "light", selectionBg: "#bfdbfe" };
|
|
13
|
+
export function getThemeForMode(mode) {
|
|
14
|
+
return mode === "light" ? LIGHT : DARK;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Honor SANTREE_THEME env override. Returns null when set to `auto` or
|
|
18
|
+
* unset/invalid — caller should fall back to terminal detection.
|
|
19
|
+
*/
|
|
20
|
+
function envOverride() {
|
|
21
|
+
const raw = process.env["SANTREE_THEME"]?.toLowerCase().trim();
|
|
22
|
+
if (raw === "light")
|
|
23
|
+
return "light";
|
|
24
|
+
if (raw === "dark")
|
|
25
|
+
return "dark";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compute relative luminance from sRGB components in the 0..1 range. Uses
|
|
30
|
+
* Rec. 709 coefficients — good enough to decide light vs dark.
|
|
31
|
+
*/
|
|
32
|
+
function luminance(r, g, b) {
|
|
33
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse an OSC 11 color response. Terminals reply with one of:
|
|
37
|
+
* \x1b]11;rgb:RRRR/GGGG/BBBB\x07
|
|
38
|
+
* \x1b]11;rgb:RR/GG/BB\x1b\\
|
|
39
|
+
* Component widths can be 2 or 4 hex digits. Returns luminance in 0..1, or
|
|
40
|
+
* null if the buffer doesn't contain a recognisable response.
|
|
41
|
+
*/
|
|
42
|
+
function parseOsc11(buf) {
|
|
43
|
+
const m = buf.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);
|
|
44
|
+
if (!m)
|
|
45
|
+
return null;
|
|
46
|
+
const conv = (hex) => parseInt(hex, 16) / (hex.length >= 4 ? 65535 : 255);
|
|
47
|
+
const r = conv(m[1]);
|
|
48
|
+
const g = conv(m[2]);
|
|
49
|
+
const b = conv(m[3]);
|
|
50
|
+
return luminance(r, g, b);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Query the terminal for its background color via OSC 11 and resolve the
|
|
54
|
+
* detected ThemeMode within `timeoutMs`. Falls back to `dark` on timeout or
|
|
55
|
+
* non-TTY stdin/stdout.
|
|
56
|
+
*
|
|
57
|
+
* The returned promise never rejects — failures resolve to `dark`.
|
|
58
|
+
*/
|
|
59
|
+
export function detectTerminalTheme(timeoutMs = 150) {
|
|
60
|
+
const override = envOverride();
|
|
61
|
+
if (override)
|
|
62
|
+
return Promise.resolve(override);
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
65
|
+
resolve("dark");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const stdin = process.stdin;
|
|
69
|
+
const wasRaw = stdin.isRaw;
|
|
70
|
+
let buf = "";
|
|
71
|
+
let settled = false;
|
|
72
|
+
const finish = (mode) => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
stdin.removeListener("data", onData);
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
if (!wasRaw) {
|
|
79
|
+
try {
|
|
80
|
+
stdin.setRawMode(false);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* ignore */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
resolve(mode);
|
|
87
|
+
};
|
|
88
|
+
const onData = (chunk) => {
|
|
89
|
+
buf += chunk.toString("utf-8");
|
|
90
|
+
const lum = parseOsc11(buf);
|
|
91
|
+
if (lum !== null)
|
|
92
|
+
finish(lum > 0.5 ? "light" : "dark");
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
if (!wasRaw)
|
|
96
|
+
stdin.setRawMode(true);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
finish("dark");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
stdin.on("data", onData);
|
|
103
|
+
const timer = setTimeout(() => finish("dark"), timeoutMs);
|
|
104
|
+
// Send the OSC 11 query. The terminal's response is consumed by onData
|
|
105
|
+
// above and never reaches Ink's input handlers.
|
|
106
|
+
try {
|
|
107
|
+
process.stdout.write("\x1b]11;?\x07");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
finish("dark");
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -23,6 +23,11 @@ export interface WorktreeInfo {
|
|
|
23
23
|
gitStatus: string;
|
|
24
24
|
sessionState: "waiting" | "idle" | "active" | null;
|
|
25
25
|
sessionMessage: string | null;
|
|
26
|
+
diffStats: {
|
|
27
|
+
filesChanged: number;
|
|
28
|
+
insertions: number;
|
|
29
|
+
deletions: number;
|
|
30
|
+
} | null;
|
|
26
31
|
}
|
|
27
32
|
export interface DashboardIssue {
|
|
28
33
|
issue: LinearAssignedIssue;
|
|
@@ -58,7 +63,13 @@ export interface EnrichedReviewPR {
|
|
|
58
63
|
worktree: WorktreeInfo | null;
|
|
59
64
|
}
|
|
60
65
|
export type DashboardTab = "issues" | "reviews";
|
|
61
|
-
export type ActionOverlay = "mode-select" | "context-input" | "base-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | null;
|
|
66
|
+
export type ActionOverlay = "mode-select" | "context-input" | "base-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | "diff" | null;
|
|
67
|
+
export type DiffFileStatus = "M" | "A" | "D" | "R" | "C" | "U" | "?";
|
|
68
|
+
export interface DiffFile {
|
|
69
|
+
path: string;
|
|
70
|
+
status: DiffFileStatus;
|
|
71
|
+
oldPath?: string;
|
|
72
|
+
}
|
|
62
73
|
export type CommitPhase = "idle" | "confirm-stage" | "awaiting-message" | "committing" | "pushing" | "done" | "error";
|
|
63
74
|
export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "filling" | "review" | "confirm" | "creating" | "done" | "error";
|
|
64
75
|
export interface DashboardState {
|
|
@@ -103,6 +114,18 @@ export interface DashboardState {
|
|
|
103
114
|
contextInputValue: string;
|
|
104
115
|
contextInputMode: "plan" | "implement" | null;
|
|
105
116
|
contextInputPhase: "editing" | "review";
|
|
117
|
+
diffTicketId: string | null;
|
|
118
|
+
diffWorktreePath: string | null;
|
|
119
|
+
diffBaseBranch: string | null;
|
|
120
|
+
diffMergeBase: string | null;
|
|
121
|
+
diffFiles: DiffFile[];
|
|
122
|
+
diffFileIndex: number;
|
|
123
|
+
diffFileScrollOffset: number;
|
|
124
|
+
diffContent: string | null;
|
|
125
|
+
diffContentScrollOffset: number;
|
|
126
|
+
diffLoadingFiles: boolean;
|
|
127
|
+
diffLoadingContent: boolean;
|
|
128
|
+
diffError: string | null;
|
|
106
129
|
}
|
|
107
130
|
export type DashboardAction = {
|
|
108
131
|
type: "SET_DATA";
|
|
@@ -237,6 +260,34 @@ export type DashboardAction = {
|
|
|
237
260
|
type: "CONTEXT_INPUT_EDIT";
|
|
238
261
|
} | {
|
|
239
262
|
type: "CONTEXT_INPUT_DONE";
|
|
263
|
+
} | {
|
|
264
|
+
type: "DIFF_OPEN";
|
|
265
|
+
ticketId: string;
|
|
266
|
+
worktreePath: string;
|
|
267
|
+
baseBranch: string;
|
|
268
|
+
} | {
|
|
269
|
+
type: "DIFF_FILES_LOADED";
|
|
270
|
+
files: DiffFile[];
|
|
271
|
+
mergeBase: string;
|
|
272
|
+
} | {
|
|
273
|
+
type: "DIFF_FILES_ERROR";
|
|
274
|
+
error: string;
|
|
275
|
+
} | {
|
|
276
|
+
type: "DIFF_FILE_SELECT";
|
|
277
|
+
index: number;
|
|
278
|
+
} | {
|
|
279
|
+
type: "DIFF_FILE_SCROLL";
|
|
280
|
+
offset: number;
|
|
281
|
+
} | {
|
|
282
|
+
type: "DIFF_CONTENT_LOADING";
|
|
283
|
+
} | {
|
|
284
|
+
type: "DIFF_CONTENT_LOADED";
|
|
285
|
+
content: string;
|
|
286
|
+
} | {
|
|
287
|
+
type: "DIFF_CONTENT_SCROLL";
|
|
288
|
+
offset: number;
|
|
289
|
+
} | {
|
|
290
|
+
type: "DIFF_CLOSE";
|
|
240
291
|
};
|
|
241
292
|
export declare const initialState: DashboardState;
|
|
242
293
|
export declare function reducer(state: DashboardState, action: DashboardAction): DashboardState;
|
|
@@ -41,6 +41,18 @@ export const initialState = {
|
|
|
41
41
|
contextInputValue: "",
|
|
42
42
|
contextInputMode: null,
|
|
43
43
|
contextInputPhase: "editing",
|
|
44
|
+
diffTicketId: null,
|
|
45
|
+
diffWorktreePath: null,
|
|
46
|
+
diffBaseBranch: null,
|
|
47
|
+
diffMergeBase: null,
|
|
48
|
+
diffFiles: [],
|
|
49
|
+
diffFileIndex: 0,
|
|
50
|
+
diffFileScrollOffset: 0,
|
|
51
|
+
diffContent: null,
|
|
52
|
+
diffContentScrollOffset: 0,
|
|
53
|
+
diffLoadingFiles: false,
|
|
54
|
+
diffLoadingContent: false,
|
|
55
|
+
diffError: null,
|
|
44
56
|
};
|
|
45
57
|
export function reducer(state, action) {
|
|
46
58
|
switch (action.type) {
|
|
@@ -273,6 +285,75 @@ export function reducer(state, action) {
|
|
|
273
285
|
contextInputValue: "",
|
|
274
286
|
contextInputPhase: "editing",
|
|
275
287
|
};
|
|
288
|
+
case "DIFF_OPEN":
|
|
289
|
+
return {
|
|
290
|
+
...state,
|
|
291
|
+
overlay: "diff",
|
|
292
|
+
diffTicketId: action.ticketId,
|
|
293
|
+
diffWorktreePath: action.worktreePath,
|
|
294
|
+
diffBaseBranch: action.baseBranch,
|
|
295
|
+
diffMergeBase: null,
|
|
296
|
+
diffFiles: [],
|
|
297
|
+
diffFileIndex: 0,
|
|
298
|
+
diffFileScrollOffset: 0,
|
|
299
|
+
diffContent: null,
|
|
300
|
+
diffContentScrollOffset: 0,
|
|
301
|
+
diffLoadingFiles: true,
|
|
302
|
+
diffLoadingContent: false,
|
|
303
|
+
diffError: null,
|
|
304
|
+
};
|
|
305
|
+
case "DIFF_FILES_LOADED":
|
|
306
|
+
return {
|
|
307
|
+
...state,
|
|
308
|
+
diffFiles: action.files,
|
|
309
|
+
diffMergeBase: action.mergeBase,
|
|
310
|
+
diffFileIndex: 0,
|
|
311
|
+
diffFileScrollOffset: 0,
|
|
312
|
+
diffLoadingFiles: false,
|
|
313
|
+
diffError: null,
|
|
314
|
+
};
|
|
315
|
+
case "DIFF_FILES_ERROR":
|
|
316
|
+
return {
|
|
317
|
+
...state,
|
|
318
|
+
diffLoadingFiles: false,
|
|
319
|
+
diffError: action.error,
|
|
320
|
+
};
|
|
321
|
+
case "DIFF_FILE_SELECT":
|
|
322
|
+
return {
|
|
323
|
+
...state,
|
|
324
|
+
diffFileIndex: action.index,
|
|
325
|
+
diffContentScrollOffset: 0,
|
|
326
|
+
};
|
|
327
|
+
case "DIFF_FILE_SCROLL":
|
|
328
|
+
return { ...state, diffFileScrollOffset: action.offset };
|
|
329
|
+
case "DIFF_CONTENT_LOADING":
|
|
330
|
+
return { ...state, diffLoadingContent: true, diffContent: null };
|
|
331
|
+
case "DIFF_CONTENT_LOADED":
|
|
332
|
+
return {
|
|
333
|
+
...state,
|
|
334
|
+
diffContent: action.content,
|
|
335
|
+
diffLoadingContent: false,
|
|
336
|
+
diffContentScrollOffset: 0,
|
|
337
|
+
};
|
|
338
|
+
case "DIFF_CONTENT_SCROLL":
|
|
339
|
+
return { ...state, diffContentScrollOffset: action.offset };
|
|
340
|
+
case "DIFF_CLOSE":
|
|
341
|
+
return {
|
|
342
|
+
...state,
|
|
343
|
+
overlay: null,
|
|
344
|
+
diffTicketId: null,
|
|
345
|
+
diffWorktreePath: null,
|
|
346
|
+
diffBaseBranch: null,
|
|
347
|
+
diffMergeBase: null,
|
|
348
|
+
diffFiles: [],
|
|
349
|
+
diffFileIndex: 0,
|
|
350
|
+
diffFileScrollOffset: 0,
|
|
351
|
+
diffContent: null,
|
|
352
|
+
diffContentScrollOffset: 0,
|
|
353
|
+
diffLoadingFiles: false,
|
|
354
|
+
diffLoadingContent: false,
|
|
355
|
+
diffError: null,
|
|
356
|
+
};
|
|
276
357
|
default:
|
|
277
358
|
return state;
|
|
278
359
|
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -190,6 +190,26 @@ export declare function getCommitsAhead(baseBranch: string): number;
|
|
|
190
190
|
* Returns 0 on failure.
|
|
191
191
|
*/
|
|
192
192
|
export declare function getCommitsAheadAsync(cwd: string, baseBranch: string): Promise<number>;
|
|
193
|
+
/**
|
|
194
|
+
* Read the SANTREE_DIFF_TOOL env var, returning the configured pager command
|
|
195
|
+
* (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
196
|
+
*
|
|
197
|
+
* The value is restricted to a safe shell-token character set since it ends
|
|
198
|
+
* up in arguments passed to spawn() — even though we never use shell:true,
|
|
199
|
+
* keeping the surface tight defends against accidental misconfigurations.
|
|
200
|
+
*/
|
|
201
|
+
export declare function getDiffTool(): string | null;
|
|
202
|
+
export interface DiffShortstat {
|
|
203
|
+
filesChanged: number;
|
|
204
|
+
insertions: number;
|
|
205
|
+
deletions: number;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Compute branch-only diff stats vs base (uses merge-base, like a GitHub PR
|
|
209
|
+
* diff). Includes both committed and uncommitted changes. Returns zeros on
|
|
210
|
+
* failure or when there are no changes.
|
|
211
|
+
*/
|
|
212
|
+
export declare function getDiffShortstatAsync(cwd: string, baseBranch: string): Promise<DiffShortstat>;
|
|
193
213
|
/**
|
|
194
214
|
* Check if a branch exists on the remote (origin).
|
|
195
215
|
* Runs: `git ls-remote --heads origin <branchName>`
|
package/dist/lib/git.js
CHANGED
|
@@ -468,6 +468,43 @@ export async function getCommitsAheadAsync(cwd, baseBranch) {
|
|
|
468
468
|
const output = await runAsync(`git -C "${cwd}" rev-list --count ${baseBranch}..HEAD`);
|
|
469
469
|
return output ? parseInt(output, 10) || 0 : 0;
|
|
470
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Read the SANTREE_DIFF_TOOL env var, returning the configured pager command
|
|
473
|
+
* (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
474
|
+
*
|
|
475
|
+
* The value is restricted to a safe shell-token character set since it ends
|
|
476
|
+
* up in arguments passed to spawn() — even though we never use shell:true,
|
|
477
|
+
* keeping the surface tight defends against accidental misconfigurations.
|
|
478
|
+
*/
|
|
479
|
+
export function getDiffTool() {
|
|
480
|
+
const raw = process.env["SANTREE_DIFF_TOOL"];
|
|
481
|
+
if (!raw || !raw.trim())
|
|
482
|
+
return null;
|
|
483
|
+
const tool = raw.trim();
|
|
484
|
+
if (!/^[a-zA-Z0-9_\-/.+]+$/.test(tool))
|
|
485
|
+
return null;
|
|
486
|
+
return tool;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Compute branch-only diff stats vs base (uses merge-base, like a GitHub PR
|
|
490
|
+
* diff). Includes both committed and uncommitted changes. Returns zeros on
|
|
491
|
+
* failure or when there are no changes.
|
|
492
|
+
*/
|
|
493
|
+
export async function getDiffShortstatAsync(cwd, baseBranch) {
|
|
494
|
+
const mergeBase = await runAsync(`git -C "${cwd}" merge-base "${baseBranch}" HEAD`);
|
|
495
|
+
const ref = mergeBase ?? baseBranch;
|
|
496
|
+
const out = await runAsync(`git -C "${cwd}" diff --shortstat "${ref}"`);
|
|
497
|
+
if (!out)
|
|
498
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
499
|
+
const fm = out.match(/(\d+) files? changed/);
|
|
500
|
+
const im = out.match(/(\d+) insertions?\(\+\)/);
|
|
501
|
+
const dm = out.match(/(\d+) deletions?\(-\)/);
|
|
502
|
+
return {
|
|
503
|
+
filesChanged: fm ? parseInt(fm[1], 10) : 0,
|
|
504
|
+
insertions: im ? parseInt(im[1], 10) : 0,
|
|
505
|
+
deletions: dm ? parseInt(dm[1], 10) : 0,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
471
508
|
/**
|
|
472
509
|
* Check if a branch exists on the remote (origin).
|
|
473
510
|
* Runs: `git ls-remote --heads origin <branchName>`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare const CURRENT_VERSION: string;
|
|
2
|
+
export declare const SANTREE_PACKAGE = "santree";
|
|
3
|
+
export declare const CLAUDE_CODE_PACKAGE = "@anthropic-ai/claude-code";
|
|
4
|
+
export type PackageManager = "npm" | "pnpm" | "yarn";
|
|
5
|
+
/**
|
|
6
|
+
* Fetch the latest published version of an npm package from the registry.
|
|
7
|
+
* Returns null on network/parse failure so callers can fall back to cache.
|
|
8
|
+
* The npm registry accepts scoped names (`@scope/name`) verbatim in the path.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchLatestVersionFor(pkgName: string, timeoutMs?: number): Promise<string | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the cached latest version of a package when fresh, otherwise refetches.
|
|
13
|
+
* Falls back to a stale cache if the network call fails.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getLatestVersionFor(pkgName: string, opts?: {
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}): Promise<string | null>;
|
|
18
|
+
/** Read a cached latest version without hitting the network. */
|
|
19
|
+
export declare function getCachedLatestVersionFor(pkgName: string): string | null;
|
|
20
|
+
export declare const fetchLatestVersion: (timeoutMs?: number) => Promise<string | null>;
|
|
21
|
+
export declare const getLatestVersion: (opts?: {
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}) => Promise<string | null>;
|
|
24
|
+
export declare const getCachedLatestVersion: () => string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Compare semver-ish versions (major.minor.patch). Pre-release tags ignored.
|
|
27
|
+
* Returns -1 if a < b, 0 if equal, 1 if a > b.
|
|
28
|
+
*/
|
|
29
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
30
|
+
export declare function isUpdateAvailable(current: string, latest: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Read the locally installed Claude Code CLI version. Probes the resolved
|
|
33
|
+
* Claude binary first (which prefers cmux's bundled copy when running inside
|
|
34
|
+
* cmux — see lib/ai.ts:resolveClaudeBinary), then falls back to `claude` on
|
|
35
|
+
* PATH and the Anthropic installer location.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getInstalledClaudeVersion(): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Detect which package manager owns the running santree binary by inspecting
|
|
40
|
+
* the resolved path of `process.argv[1]`. Falls back to npm when uncertain.
|
|
41
|
+
*
|
|
42
|
+
* Common install paths:
|
|
43
|
+
* pnpm → ~/Library/pnpm/global/..., .../node_modules/.pnpm/santree@.../
|
|
44
|
+
* yarn → ~/.config/yarn/global/..., ~/.yarn/...
|
|
45
|
+
* npm → /usr/local/lib/node_modules/santree/..., /opt/homebrew/...
|
|
46
|
+
*/
|
|
47
|
+
export declare function detectPackageManager(): PackageManager;
|
|
48
|
+
export interface InstallCommand {
|
|
49
|
+
cmd: string;
|
|
50
|
+
args: string[];
|
|
51
|
+
display: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function getInstallCommandFor(pm: PackageManager, packageSpec: string): InstallCommand;
|
|
54
|
+
/** Convenience: install the latest santree via the detected manager. */
|
|
55
|
+
export declare function getInstallCommand(pm: PackageManager): InstallCommand;
|