santree 0.5.3 → 0.5.5
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 +156 -46
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +22 -18
- package/dist/commands/doctor.js +97 -76
- 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/english-tutor/index.d.ts +1 -0
- package/dist/commands/helpers/english-tutor/index.js +1 -0
- package/dist/commands/helpers/english-tutor/install.d.ts +8 -0
- package/dist/commands/helpers/english-tutor/install.js +24 -0
- package/dist/commands/helpers/english-tutor/prompt.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/prompt.js +16 -0
- package/dist/commands/helpers/english-tutor/session-start.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/session-start.js +34 -0
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/uninstall.js +15 -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/data.js +17 -9
- package/dist/lib/dashboard/types.d.ts +3 -16
- package/dist/lib/english-tutor.d.ts +13 -0
- package/dist/lib/english-tutor.js +125 -0
- 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/index.js +5 -12
- 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/english-tutor-prompt.njk +15 -0
- 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
|
@@ -4,11 +4,11 @@ import { Text, Box } from "ink";
|
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { exec } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import { findMainRepoRoot, getCurrentBranch
|
|
8
|
-
import {
|
|
7
|
+
import { findMainRepoRoot, getCurrentBranch } from "../../lib/git.js";
|
|
8
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
9
9
|
const execAsync = promisify(exec);
|
|
10
|
-
export const description = "Open the current
|
|
11
|
-
export default function
|
|
10
|
+
export const description = "Open the current branch's issue in the browser";
|
|
11
|
+
export default function IssueOpen() {
|
|
12
12
|
const [status, setStatus] = useState("checking");
|
|
13
13
|
const [message, setMessage] = useState("");
|
|
14
14
|
useEffect(() => {
|
|
@@ -26,16 +26,18 @@ export default function LinearOpen() {
|
|
|
26
26
|
setMessage("Could not determine current branch");
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
const
|
|
29
|
+
const tracker = getIssueTracker(repoRoot);
|
|
30
|
+
const ticketId = tracker.extractIdFromBranch(branch);
|
|
30
31
|
if (!ticketId) {
|
|
31
32
|
setStatus("error");
|
|
32
|
-
setMessage(
|
|
33
|
+
setMessage(`No ${tracker.issueNoun} ID found in branch '${branch}'`);
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
36
|
+
const result = await tracker.getIssue(ticketId, repoRoot);
|
|
37
|
+
if (!result.ok || !result.value.url) {
|
|
38
|
+
const auth = await tracker.getAuthStatus(repoRoot);
|
|
37
39
|
setStatus("error");
|
|
38
|
-
setMessage(`Could not fetch
|
|
40
|
+
setMessage(`Could not fetch ${tracker.issueNoun} ${ticketId}.${auth.hint ? ` ${auth.hint}` : ""}`);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
try {
|
|
@@ -44,7 +46,7 @@ export default function LinearOpen() {
|
|
|
44
46
|
: process.platform === "win32"
|
|
45
47
|
? "start"
|
|
46
48
|
: "xdg-open";
|
|
47
|
-
await execAsync(`${openCmd} "${
|
|
49
|
+
await execAsync(`${openCmd} "${result.value.url}"`);
|
|
48
50
|
setStatus("done");
|
|
49
51
|
setMessage(`Opened ${ticketId} in browser`);
|
|
50
52
|
}
|
|
@@ -61,5 +63,5 @@ export default function LinearOpen() {
|
|
|
61
63
|
return () => clearTimeout(timer);
|
|
62
64
|
}
|
|
63
65
|
}, [status]);
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Opening
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Opening issue..." })] })), status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] }));
|
|
65
67
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
export declare const description = "Switch the active issue tracker for this repo";
|
|
3
|
+
export declare const args: z.ZodTuple<[z.ZodEnum<{
|
|
4
|
+
linear: "linear";
|
|
5
|
+
github: "github";
|
|
6
|
+
}>], null>;
|
|
7
|
+
type Props = {
|
|
8
|
+
args: z.infer<typeof args>;
|
|
9
|
+
};
|
|
10
|
+
export default function IssueSwitch({ args }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text, Box } from "ink";
|
|
4
|
+
import { argument } from "pastel";
|
|
5
|
+
import { z } from "zod/v4";
|
|
6
|
+
import { findMainRepoRoot } from "../../lib/git.js";
|
|
7
|
+
import { setRepoTracker, getIssueTracker } from "../../lib/trackers/index.js";
|
|
8
|
+
export const description = "Switch the active issue tracker for this repo";
|
|
9
|
+
export const args = z.tuple([
|
|
10
|
+
z.enum(["linear", "github"]).describe(argument({
|
|
11
|
+
name: "kind",
|
|
12
|
+
description: "Tracker kind: linear or github",
|
|
13
|
+
})),
|
|
14
|
+
]);
|
|
15
|
+
export default function IssueSwitch({ args }) {
|
|
16
|
+
const [kind] = args;
|
|
17
|
+
const [status, setStatus] = useState("switching");
|
|
18
|
+
const [message, setMessage] = useState("");
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const repoRoot = findMainRepoRoot();
|
|
21
|
+
if (!repoRoot) {
|
|
22
|
+
setMessage("Not inside a git repository");
|
|
23
|
+
setStatus("error");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setRepoTracker(repoRoot, kind);
|
|
27
|
+
const tracker = getIssueTracker(repoRoot);
|
|
28
|
+
setMessage(`Active tracker for this repo: ${tracker.displayName}`);
|
|
29
|
+
setStatus("done");
|
|
30
|
+
}, [kind]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (status === "done" || status === "error") {
|
|
33
|
+
const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 50);
|
|
34
|
+
return () => clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}, [status]);
|
|
37
|
+
return (_jsxs(Box, { padding: 1, children: [status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] }));
|
|
38
|
+
}
|
|
@@ -3,8 +3,10 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box, useInput } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { findMainRepoRoot
|
|
7
|
-
import {
|
|
6
|
+
import { findMainRepoRoot } from "../../lib/git.js";
|
|
7
|
+
import { setRepoLinearOrg, getRepoLinearOrg, removeRepoLinearOrg, startOAuthFlow, getValidTokens, linearTracker, } from "../../lib/trackers/linear/index.js";
|
|
8
|
+
import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
|
|
9
|
+
import { setRepoTracker } from "../../lib/trackers/index.js";
|
|
8
10
|
import { renderTicket } from "../../lib/prompts.js";
|
|
9
11
|
export const description = "Authenticate with Linear";
|
|
10
12
|
export const options = z.object({
|
|
@@ -46,6 +48,7 @@ export default function LinearAuth({ options }) {
|
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
50
|
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
51
|
+
setRepoTracker(repoRoot, "linear");
|
|
49
52
|
setMessage(`Authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
50
53
|
setStatus("done");
|
|
51
54
|
});
|
|
@@ -54,6 +57,7 @@ export default function LinearAuth({ options }) {
|
|
|
54
57
|
// Link existing org
|
|
55
58
|
const choice = choices[selected];
|
|
56
59
|
setRepoLinearOrg(repoRoot, choice.slug);
|
|
60
|
+
setRepoTracker(repoRoot, "linear");
|
|
57
61
|
setMessage(`Linked repo to ${choice.name} (${choice.slug})`);
|
|
58
62
|
setStatus("done");
|
|
59
63
|
}
|
|
@@ -67,35 +71,42 @@ export default function LinearAuth({ options }) {
|
|
|
67
71
|
setStatus("error");
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
74
|
+
const result = await linearTracker.getIssue(options.test, repoRoot);
|
|
75
|
+
if (!result.ok) {
|
|
72
76
|
setError(`Could not fetch ticket ${options.test}. Check auth and ticket ID.`);
|
|
73
77
|
setStatus("error");
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
|
-
setMessage(renderTicket(
|
|
80
|
+
setMessage(renderTicket(result.value, linearTracker.displayName).trim());
|
|
77
81
|
setStatus("done");
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
80
84
|
if (options.status) {
|
|
81
85
|
const repoRoot = findMainRepoRoot();
|
|
82
|
-
const authStatus = getAuthStatus(repoRoot);
|
|
86
|
+
const authStatus = await linearTracker.getAuthStatus(repoRoot);
|
|
83
87
|
if (!authStatus.authenticated) {
|
|
84
88
|
setMessage("Not authenticated with Linear");
|
|
85
89
|
setStatus("done");
|
|
86
90
|
return;
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
const orgSlug = repoRoot ? getRepoLinearOrg(repoRoot) : null;
|
|
93
|
+
if (orgSlug) {
|
|
94
|
+
const valid = await getValidTokens(orgSlug);
|
|
90
95
|
const expiry = valid
|
|
91
96
|
? new Date(valid.expires_at).toLocaleString()
|
|
92
97
|
: "expired (refresh failed)";
|
|
93
98
|
setMessage([
|
|
94
|
-
`
|
|
99
|
+
`Account: ${authStatus.accountLabel ?? orgSlug}`,
|
|
95
100
|
`Token expires: ${expiry}`,
|
|
96
101
|
`Repo linked: ${authStatus.repoLinked ? "yes" : "no"}`,
|
|
97
102
|
].join("\n"));
|
|
98
103
|
}
|
|
104
|
+
else {
|
|
105
|
+
setMessage([
|
|
106
|
+
`Account: ${authStatus.accountLabel ?? "unknown"}`,
|
|
107
|
+
`Repo linked: ${authStatus.repoLinked ? "yes" : "no"}`,
|
|
108
|
+
].join("\n"));
|
|
109
|
+
}
|
|
99
110
|
setStatus("done");
|
|
100
111
|
return;
|
|
101
112
|
}
|
|
@@ -142,12 +153,13 @@ export default function LinearAuth({ options }) {
|
|
|
142
153
|
return;
|
|
143
154
|
}
|
|
144
155
|
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
156
|
+
setRepoTracker(repoRoot, "linear");
|
|
145
157
|
setMessage(`Re-authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
146
158
|
setStatus("done");
|
|
147
159
|
return;
|
|
148
160
|
}
|
|
149
161
|
// Check for existing authenticated orgs
|
|
150
|
-
const store =
|
|
162
|
+
const store = readLinearAuthStore();
|
|
151
163
|
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
152
164
|
slug,
|
|
153
165
|
name: tokens.org_name,
|
|
@@ -162,6 +174,7 @@ export default function LinearAuth({ options }) {
|
|
|
162
174
|
return;
|
|
163
175
|
}
|
|
164
176
|
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
177
|
+
setRepoTracker(repoRoot, "linear");
|
|
165
178
|
setMessage(`Authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
166
179
|
setStatus("done");
|
|
167
180
|
return;
|
|
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box, useInput } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import { findMainRepoRoot
|
|
6
|
-
import {
|
|
5
|
+
import { findMainRepoRoot } from "../../lib/git.js";
|
|
6
|
+
import { setRepoLinearOrg, getRepoLinearOrg } from "../../lib/trackers/linear/index.js";
|
|
7
|
+
import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
|
|
8
|
+
import { setRepoTracker } from "../../lib/trackers/index.js";
|
|
7
9
|
export const description = "Switch Linear workspace for this repo";
|
|
8
10
|
export default function LinearSwitch() {
|
|
9
11
|
const [status, setStatus] = useState("checking");
|
|
@@ -25,6 +27,7 @@ export default function LinearSwitch() {
|
|
|
25
27
|
const choice = choices[selected];
|
|
26
28
|
const repoRoot = findMainRepoRoot();
|
|
27
29
|
setRepoLinearOrg(repoRoot, choice.slug);
|
|
30
|
+
setRepoTracker(repoRoot, "linear");
|
|
28
31
|
setMessage(`Switched to ${choice.name} (${choice.slug})`);
|
|
29
32
|
setStatus("done");
|
|
30
33
|
}
|
|
@@ -38,7 +41,7 @@ export default function LinearSwitch() {
|
|
|
38
41
|
setStatus("error");
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
const store =
|
|
44
|
+
const store = readLinearAuthStore();
|
|
42
45
|
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
43
46
|
slug,
|
|
44
47
|
name: tokens.org_name,
|
|
@@ -51,6 +54,7 @@ export default function LinearSwitch() {
|
|
|
51
54
|
if (orgs.length === 1) {
|
|
52
55
|
const org = orgs[0];
|
|
53
56
|
setRepoLinearOrg(repoRoot, org.slug);
|
|
57
|
+
setRepoTracker(repoRoot, "linear");
|
|
54
58
|
setMessage(`Linked to ${org.name} (${org.slug})`);
|
|
55
59
|
setStatus("done");
|
|
56
60
|
return;
|
|
@@ -12,7 +12,7 @@ import { findMainRepoRoot, findRepoRoot, getCurrentBranch, getBaseBranch, hasUnc
|
|
|
12
12
|
import { ghCliAvailable, getPRInfoAsync, pushBranch, createPR, getPRTemplate, } from "../../lib/github.js";
|
|
13
13
|
import { renderPrompt, renderDiff, renderTicket } from "../../lib/prompts.js";
|
|
14
14
|
import { runAgent } from "../../lib/ai.js";
|
|
15
|
-
import {
|
|
15
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
16
16
|
const execAsync = promisify(exec);
|
|
17
17
|
export const description = "Create a GitHub pull request";
|
|
18
18
|
export const options = z.object({
|
|
@@ -63,12 +63,14 @@ export default function PR({ options }) {
|
|
|
63
63
|
}
|
|
64
64
|
const ticketId = extractTicketId(branch);
|
|
65
65
|
const mainRepoRoot = findMainRepoRoot();
|
|
66
|
-
// Fetch
|
|
66
|
+
// Fetch issue content from the active tracker (downloads images
|
|
67
|
+
// inline so Claude can read them via --allowedTools Read).
|
|
67
68
|
let ticketContent;
|
|
68
69
|
if (ticketId && mainRepoRoot) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const tracker = getIssueTracker(mainRepoRoot);
|
|
71
|
+
const result = await tracker.getIssue(ticketId, mainRepoRoot);
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
ticketContent = renderTicket(result.value, tracker.displayName);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
const diffContent = renderDiff({
|
|
@@ -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` (real cmux runtime) — outside a live
|
|
135
|
+
// workspace the bundled binary has no auth context and exits with
|
|
136
|
+
// "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" });
|
|
@@ -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;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function getLogPath(): string;
|
|
2
|
+
export declare function getHooksJson(): Record<string, unknown>;
|
|
3
|
+
export declare function getPermissionEntry(): string;
|
|
4
|
+
export declare function installHooks(): {
|
|
5
|
+
settingsPath: string;
|
|
6
|
+
logPath: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Remove hooks and permission entry. Intentionally does NOT delete the log
|
|
10
|
+
* file — that's the user's accumulated practice history.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uninstallHooks(): string;
|
|
13
|
+
export declare function getInstallSnippet(): Record<string, unknown>;
|