santree 0.1.3 → 0.1.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 +1 -0
- package/dist/commands/linear/switch.d.ts +2 -0
- package/dist/commands/linear/switch.js +71 -0
- package/dist/commands/worktree/remove.d.ts +4 -2
- package/dist/commands/worktree/remove.js +60 -19
- package/dist/lib/github.d.ts +6 -49
- package/dist/lib/github.js +6 -189
- package/package.json +1 -1
- package/prompts/fix-pr.njk +3 -1
- package/prompts/review.njk +3 -1
- package/prompts/work.njk +3 -1
package/README.md
CHANGED
|
@@ -110,6 +110,7 @@ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw c
|
|
|
110
110
|
| Command | Description |
|
|
111
111
|
|---------|-------------|
|
|
112
112
|
| `santree linear auth` | Authenticate with Linear (OAuth) |
|
|
113
|
+
| `santree linear switch` | Switch Linear workspace for this repo |
|
|
113
114
|
| `santree linear open` | Open the current Linear ticket in the browser |
|
|
114
115
|
|
|
115
116
|
### Helpers (`santree helpers`)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text, Box, useInput } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { findMainRepoRoot, setRepoLinearOrg, getRepoLinearOrg } from "../../lib/git.js";
|
|
6
|
+
import { readAuthStore } from "../../lib/linear.js";
|
|
7
|
+
export const description = "Switch Linear workspace for this repo";
|
|
8
|
+
export default function LinearSwitch() {
|
|
9
|
+
const [status, setStatus] = useState("checking");
|
|
10
|
+
const [message, setMessage] = useState("");
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const [choices, setChoices] = useState([]);
|
|
13
|
+
const [selected, setSelected] = useState(0);
|
|
14
|
+
const [currentOrg, setCurrentOrg] = useState(null);
|
|
15
|
+
useInput((input, key) => {
|
|
16
|
+
if (status !== "choosing")
|
|
17
|
+
return;
|
|
18
|
+
if (key.upArrow) {
|
|
19
|
+
setSelected((s) => Math.max(0, s - 1));
|
|
20
|
+
}
|
|
21
|
+
else if (key.downArrow) {
|
|
22
|
+
setSelected((s) => Math.min(choices.length - 1, s + 1));
|
|
23
|
+
}
|
|
24
|
+
else if (key.return) {
|
|
25
|
+
const choice = choices[selected];
|
|
26
|
+
const repoRoot = findMainRepoRoot();
|
|
27
|
+
setRepoLinearOrg(repoRoot, choice.slug);
|
|
28
|
+
setMessage(`Switched to ${choice.name} (${choice.slug})`);
|
|
29
|
+
setStatus("done");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
async function run() {
|
|
34
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
35
|
+
const repoRoot = findMainRepoRoot();
|
|
36
|
+
if (!repoRoot) {
|
|
37
|
+
setError("Not inside a git repository");
|
|
38
|
+
setStatus("error");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const store = readAuthStore();
|
|
42
|
+
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
43
|
+
slug,
|
|
44
|
+
name: tokens.org_name,
|
|
45
|
+
}));
|
|
46
|
+
if (orgs.length === 0) {
|
|
47
|
+
setError("No authenticated workspaces. Run: santree linear auth");
|
|
48
|
+
setStatus("error");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (orgs.length === 1) {
|
|
52
|
+
const org = orgs[0];
|
|
53
|
+
setRepoLinearOrg(repoRoot, org.slug);
|
|
54
|
+
setMessage(`Linked to ${org.name} (${org.slug})`);
|
|
55
|
+
setStatus("done");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setCurrentOrg(getRepoLinearOrg(repoRoot));
|
|
59
|
+
setChoices(orgs);
|
|
60
|
+
setStatus("choosing");
|
|
61
|
+
}
|
|
62
|
+
run();
|
|
63
|
+
}, []);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (status === "done" || status === "error") {
|
|
66
|
+
const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 100);
|
|
67
|
+
return () => clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}, [status]);
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Linear Switch" }) }), status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking..." })] })), status === "choosing" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a workspace to link to this repo:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: choices.map((org, i) => (_jsxs(Text, { children: [i === selected ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), org.name, " (", org.slug, ")", org.slug === currentOrg && _jsx(Text, { dimColor: true, children: " (current)" })] }, org.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), status === "done" && _jsxs(Text, { color: "green", children: ["\u2713 ", message] }), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
|
|
71
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const description = "Remove a worktree and its branch";
|
|
3
|
-
export declare const options: z.ZodObject<{
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>;
|
|
4
6
|
export declare const args: z.ZodTuple<[z.ZodString], null>;
|
|
5
7
|
type Props = {
|
|
6
8
|
options: z.infer<typeof options>;
|
|
7
9
|
args: z.infer<typeof args>;
|
|
8
10
|
};
|
|
9
|
-
export default function Remove({ args }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export default function Remove({ args, options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
12
|
export {};
|
|
@@ -1,40 +1,81 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { Text, Box } from "ink";
|
|
3
|
+
import { Text, Box, useInput, useApp } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { removeWorktree, findMainRepoRoot } from "../../lib/git.js";
|
|
7
7
|
export const description = "Remove a worktree and its branch";
|
|
8
|
-
export const options = z.object({
|
|
8
|
+
export const options = z.object({
|
|
9
|
+
force: z.boolean().optional().describe("Skip confirmation prompt"),
|
|
10
|
+
});
|
|
9
11
|
export const args = z.tuple([z.string().describe("Branch name to remove")]);
|
|
10
|
-
export default function Remove({ args }) {
|
|
12
|
+
export default function Remove({ args, options }) {
|
|
11
13
|
const [branchName] = args;
|
|
12
|
-
const
|
|
14
|
+
const { exit } = useApp();
|
|
15
|
+
const [status, setStatus] = useState("checking");
|
|
13
16
|
const [message, setMessage] = useState("");
|
|
17
|
+
const [repoRoot, setRepoRoot] = useState(null);
|
|
18
|
+
useInput((input) => {
|
|
19
|
+
if (status !== "confirming")
|
|
20
|
+
return;
|
|
21
|
+
if (input === "y" || input === "Y") {
|
|
22
|
+
doRemove();
|
|
23
|
+
}
|
|
24
|
+
else if (input === "n" || input === "N" || input === "\x03") {
|
|
25
|
+
setStatus("cancelled");
|
|
26
|
+
setMessage("Cancelled");
|
|
27
|
+
setTimeout(() => exit(), 100);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
async function doRemove() {
|
|
31
|
+
if (!repoRoot)
|
|
32
|
+
return;
|
|
33
|
+
setStatus("removing");
|
|
34
|
+
setMessage(`Removing worktree ${branchName}...`);
|
|
35
|
+
const result = await removeWorktree(branchName, repoRoot, true);
|
|
36
|
+
if (result.success) {
|
|
37
|
+
setStatus("done");
|
|
38
|
+
setMessage(`Removed worktree and branch: ${branchName}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
setStatus("error");
|
|
42
|
+
setMessage(result.error ?? "Unknown error");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
14
45
|
useEffect(() => {
|
|
15
46
|
async function run() {
|
|
16
|
-
// Small delay to allow spinner to render
|
|
17
47
|
await new Promise((r) => setTimeout(r, 100));
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
48
|
+
const root = findMainRepoRoot();
|
|
49
|
+
if (!root) {
|
|
20
50
|
setStatus("error");
|
|
21
51
|
setMessage("Not inside a git repository");
|
|
22
52
|
return;
|
|
23
53
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
setRepoRoot(root);
|
|
55
|
+
if (options.force) {
|
|
56
|
+
setStatus("removing");
|
|
57
|
+
setMessage(`Removing worktree ${branchName}...`);
|
|
58
|
+
const result = await removeWorktree(branchName, root, true);
|
|
59
|
+
if (result.success) {
|
|
60
|
+
setStatus("done");
|
|
61
|
+
setMessage(`Removed worktree and branch: ${branchName}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
setStatus("error");
|
|
65
|
+
setMessage(result.error ?? "Unknown error");
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
34
68
|
}
|
|
69
|
+
setStatus("confirming");
|
|
35
70
|
}
|
|
36
71
|
run();
|
|
37
72
|
}, [branchName]);
|
|
38
|
-
|
|
39
|
-
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (status === "done" || status === "error") {
|
|
75
|
+
const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 100);
|
|
76
|
+
return () => clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
}, [status]);
|
|
79
|
+
const isLoading = status === "checking" || status === "removing";
|
|
80
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83D\uDDD1\uFE0F Remove" }) }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : status === "done" ? "green" : "yellow", paddingX: 1, width: "100%", children: _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "red", bold: true, children: branchName })] }) }), _jsxs(Box, { marginTop: 1, children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message || "Removing..."] })] })), status === "confirming" && (_jsxs(Text, { bold: true, color: "yellow", children: ["Remove this worktree and delete the branch? [y/N]:", " "] })), status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "cancelled" && _jsxs(Text, { color: "yellow", children: ["\u2717 ", message] }), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] })] }));
|
|
40
81
|
}
|
package/dist/lib/github.d.ts
CHANGED
|
@@ -4,13 +4,7 @@ export interface PRInfo {
|
|
|
4
4
|
url?: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Get PR info
|
|
8
|
-
* Runs: `gh pr view "<branchName>" --json number,state,url`
|
|
9
|
-
* Returns null if no PR exists for the branch or gh CLI fails.
|
|
10
|
-
*/
|
|
11
|
-
export declare function getPRInfo(branchName: string): PRInfo | null;
|
|
12
|
-
/**
|
|
13
|
-
* Async version of getPRInfo. Get PR info for a branch using the GitHub CLI.
|
|
7
|
+
* Get PR info for a branch using the GitHub CLI (async).
|
|
14
8
|
* Runs: `gh pr view "<branchName>" --json number,state,url`
|
|
15
9
|
* Returns null if no PR exists for the branch or gh CLI fails.
|
|
16
10
|
*/
|
|
@@ -42,29 +36,23 @@ export declare function createPR(title: string, baseBranch: string, headBranch:
|
|
|
42
36
|
*/
|
|
43
37
|
export declare function getPRTemplate(): string | null;
|
|
44
38
|
/**
|
|
45
|
-
* Fetch
|
|
46
|
-
* Runs: `gh pr view <prNumber> --json comments --jq '.comments[] | "- \(.author.login): \(.body)"'`
|
|
47
|
-
* Returns empty string if the PR has no comments or on failure.
|
|
48
|
-
*/
|
|
49
|
-
export declare function getPRComments(prNumber: string): string;
|
|
50
|
-
/**
|
|
51
|
-
* Async version of getPRChecks.
|
|
39
|
+
* Fetch CI check results for a pull request (async).
|
|
52
40
|
*/
|
|
53
41
|
export declare function getPRChecksAsync(prNumber: string): Promise<PRCheck[] | null>;
|
|
54
42
|
/**
|
|
55
|
-
*
|
|
43
|
+
* Fetch reviews for a pull request (async).
|
|
56
44
|
*/
|
|
57
45
|
export declare function getPRReviewsAsync(prNumber: string): Promise<PRReview[] | null>;
|
|
58
46
|
/**
|
|
59
|
-
*
|
|
47
|
+
* Fetch inline review comments for a pull request via the GitHub API (async).
|
|
60
48
|
*/
|
|
61
49
|
export declare function getPRReviewCommentsAsync(prNumber: string): Promise<PRReviewComment[] | null>;
|
|
62
50
|
/**
|
|
63
|
-
*
|
|
51
|
+
* Fetch structured conversation comments on a pull request (async).
|
|
64
52
|
*/
|
|
65
53
|
export declare function getPRConversationCommentsAsync(prNumber: string): Promise<PRConversationComment[] | null>;
|
|
66
54
|
/**
|
|
67
|
-
*
|
|
55
|
+
* Fetch details for a failed CI check (async): which step failed and the failed step's log.
|
|
68
56
|
*/
|
|
69
57
|
export declare function getFailedCheckDetailsAsync(check: PRCheck): Promise<FailedCheckDetail>;
|
|
70
58
|
export interface PRConversationComment {
|
|
@@ -72,12 +60,6 @@ export interface PRConversationComment {
|
|
|
72
60
|
body: string;
|
|
73
61
|
createdAt: string;
|
|
74
62
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Fetch structured conversation comments on a pull request.
|
|
77
|
-
* Runs: `gh pr view <prNumber> --json comments`
|
|
78
|
-
* Returns null if gh CLI fails.
|
|
79
|
-
*/
|
|
80
|
-
export declare function getPRConversationComments(prNumber: string): PRConversationComment[] | null;
|
|
81
63
|
export interface PRCheck {
|
|
82
64
|
name: string;
|
|
83
65
|
state: string;
|
|
@@ -94,13 +76,6 @@ export interface FailedCheckDetail {
|
|
|
94
76
|
failed_step: string | null;
|
|
95
77
|
log: string | null;
|
|
96
78
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Fetch details for a failed CI check: which step failed and the failed step's log.
|
|
99
|
-
* Extracts job ID from the check link, fetches job details for the step name,
|
|
100
|
-
* then fetches the job log via the GitHub API and extracts the failed step's output.
|
|
101
|
-
* Returns enriched detail; gracefully degrades if API calls fail.
|
|
102
|
-
*/
|
|
103
|
-
export declare function getFailedCheckDetails(check: PRCheck): FailedCheckDetail;
|
|
104
79
|
export interface PRReview {
|
|
105
80
|
author: {
|
|
106
81
|
login: string;
|
|
@@ -122,21 +97,3 @@ export interface PRReviewComment {
|
|
|
122
97
|
in_reply_to_id?: number;
|
|
123
98
|
id: number;
|
|
124
99
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Fetch CI check results for a pull request.
|
|
127
|
-
* Runs: `gh pr checks <prNumber> --json name,state,bucket,link,description,workflow`
|
|
128
|
-
* Returns null if gh CLI fails.
|
|
129
|
-
*/
|
|
130
|
-
export declare function getPRChecks(prNumber: string): PRCheck[] | null;
|
|
131
|
-
/**
|
|
132
|
-
* Fetch reviews for a pull request.
|
|
133
|
-
* Runs: `gh pr view <prNumber> --json reviews`
|
|
134
|
-
* Returns null if gh CLI fails.
|
|
135
|
-
*/
|
|
136
|
-
export declare function getPRReviews(prNumber: string): PRReview[] | null;
|
|
137
|
-
/**
|
|
138
|
-
* Fetch inline review comments for a pull request via the GitHub API.
|
|
139
|
-
* Runs: `gh api repos/{owner}/{repo}/pulls/<prNumber>/comments --paginate`
|
|
140
|
-
* Returns null if gh CLI fails.
|
|
141
|
-
*/
|
|
142
|
-
export declare function getPRReviewComments(prNumber: string): PRReviewComment[] | null;
|
package/dist/lib/github.js
CHANGED
|
@@ -3,28 +3,7 @@ import { promisify } from "util";
|
|
|
3
3
|
import { run, runAsync } from "./exec.js";
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
/**
|
|
6
|
-
* Get PR info
|
|
7
|
-
* Runs: `gh pr view "<branchName>" --json number,state,url`
|
|
8
|
-
* Returns null if no PR exists for the branch or gh CLI fails.
|
|
9
|
-
*/
|
|
10
|
-
export function getPRInfo(branchName) {
|
|
11
|
-
const output = run(`gh pr view "${branchName}" --json number,state,url`);
|
|
12
|
-
if (!output)
|
|
13
|
-
return null;
|
|
14
|
-
try {
|
|
15
|
-
const data = JSON.parse(output);
|
|
16
|
-
return {
|
|
17
|
-
number: String(data.number ?? ""),
|
|
18
|
-
state: data.state ?? "OPEN",
|
|
19
|
-
url: data.url,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Async version of getPRInfo. Get PR info for a branch using the GitHub CLI.
|
|
6
|
+
* Get PR info for a branch using the GitHub CLI (async).
|
|
28
7
|
* Runs: `gh pr view "<branchName>" --json number,state,url`
|
|
29
8
|
* Returns null if no PR exists for the branch or gh CLI fails.
|
|
30
9
|
*/
|
|
@@ -102,15 +81,7 @@ export function getPRTemplate() {
|
|
|
102
81
|
return Buffer.from(output, "base64").toString("utf-8");
|
|
103
82
|
}
|
|
104
83
|
/**
|
|
105
|
-
* Fetch
|
|
106
|
-
* Runs: `gh pr view <prNumber> --json comments --jq '.comments[] | "- \(.author.login): \(.body)"'`
|
|
107
|
-
* Returns empty string if the PR has no comments or on failure.
|
|
108
|
-
*/
|
|
109
|
-
export function getPRComments(prNumber) {
|
|
110
|
-
return (run(`gh pr view ${prNumber} --json comments --jq '.comments[] | "- \\(.author.login): \\(.body)"'`) ?? "");
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Async version of getPRChecks.
|
|
84
|
+
* Fetch CI check results for a pull request (async).
|
|
114
85
|
*/
|
|
115
86
|
export async function getPRChecksAsync(prNumber) {
|
|
116
87
|
const output = await runAsync(`gh pr checks ${prNumber} --json name,state,bucket,link,description,workflow`);
|
|
@@ -124,7 +95,7 @@ export async function getPRChecksAsync(prNumber) {
|
|
|
124
95
|
}
|
|
125
96
|
}
|
|
126
97
|
/**
|
|
127
|
-
*
|
|
98
|
+
* Fetch reviews for a pull request (async).
|
|
128
99
|
*/
|
|
129
100
|
export async function getPRReviewsAsync(prNumber) {
|
|
130
101
|
const output = await runAsync(`gh pr view ${prNumber} --json reviews`);
|
|
@@ -139,7 +110,7 @@ export async function getPRReviewsAsync(prNumber) {
|
|
|
139
110
|
}
|
|
140
111
|
}
|
|
141
112
|
/**
|
|
142
|
-
*
|
|
113
|
+
* Fetch inline review comments for a pull request via the GitHub API (async).
|
|
143
114
|
*/
|
|
144
115
|
export async function getPRReviewCommentsAsync(prNumber) {
|
|
145
116
|
const output = await runAsync(`gh api repos/{owner}/{repo}/pulls/${prNumber}/comments --paginate`);
|
|
@@ -153,7 +124,7 @@ export async function getPRReviewCommentsAsync(prNumber) {
|
|
|
153
124
|
}
|
|
154
125
|
}
|
|
155
126
|
/**
|
|
156
|
-
*
|
|
127
|
+
* Fetch structured conversation comments on a pull request (async).
|
|
157
128
|
*/
|
|
158
129
|
export async function getPRConversationCommentsAsync(prNumber) {
|
|
159
130
|
const output = await runAsync(`gh pr view ${prNumber} --json comments`);
|
|
@@ -172,7 +143,7 @@ export async function getPRConversationCommentsAsync(prNumber) {
|
|
|
172
143
|
}
|
|
173
144
|
}
|
|
174
145
|
/**
|
|
175
|
-
*
|
|
146
|
+
* Fetch details for a failed CI check (async): which step failed and the failed step's log.
|
|
176
147
|
*/
|
|
177
148
|
export async function getFailedCheckDetailsAsync(check) {
|
|
178
149
|
const detail = {
|
|
@@ -245,157 +216,3 @@ export async function getFailedCheckDetailsAsync(check) {
|
|
|
245
216
|
}
|
|
246
217
|
return detail;
|
|
247
218
|
}
|
|
248
|
-
/**
|
|
249
|
-
* Fetch structured conversation comments on a pull request.
|
|
250
|
-
* Runs: `gh pr view <prNumber> --json comments`
|
|
251
|
-
* Returns null if gh CLI fails.
|
|
252
|
-
*/
|
|
253
|
-
export function getPRConversationComments(prNumber) {
|
|
254
|
-
const output = run(`gh pr view ${prNumber} --json comments`);
|
|
255
|
-
if (!output)
|
|
256
|
-
return null;
|
|
257
|
-
try {
|
|
258
|
-
const data = JSON.parse(output);
|
|
259
|
-
return (data.comments ?? []).map((c) => ({
|
|
260
|
-
author: c.author?.login ?? "unknown",
|
|
261
|
-
body: c.body ?? "",
|
|
262
|
-
createdAt: c.createdAt ?? "",
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Fetch details for a failed CI check: which step failed and the failed step's log.
|
|
271
|
-
* Extracts job ID from the check link, fetches job details for the step name,
|
|
272
|
-
* then fetches the job log via the GitHub API and extracts the failed step's output.
|
|
273
|
-
* Returns enriched detail; gracefully degrades if API calls fail.
|
|
274
|
-
*/
|
|
275
|
-
export function getFailedCheckDetails(check) {
|
|
276
|
-
const detail = {
|
|
277
|
-
name: check.name,
|
|
278
|
-
workflow: check.workflow,
|
|
279
|
-
description: check.description,
|
|
280
|
-
link: check.link,
|
|
281
|
-
failed_step: null,
|
|
282
|
-
log: null,
|
|
283
|
-
};
|
|
284
|
-
const urlMatch = check.link?.match(/job\/(\d+)/);
|
|
285
|
-
if (!urlMatch)
|
|
286
|
-
return detail;
|
|
287
|
-
const jobId = urlMatch[1];
|
|
288
|
-
let stepStartMs = 0;
|
|
289
|
-
let stepEndMs = 0;
|
|
290
|
-
const jobOutput = run(`gh api repos/{owner}/{repo}/actions/jobs/${jobId}`);
|
|
291
|
-
if (jobOutput) {
|
|
292
|
-
try {
|
|
293
|
-
const job = JSON.parse(jobOutput);
|
|
294
|
-
const failedStep = job.steps?.find((s) => s.conclusion === "failure");
|
|
295
|
-
if (failedStep) {
|
|
296
|
-
detail.failed_step = failedStep.name;
|
|
297
|
-
stepStartMs = new Date(failedStep.started_at).getTime();
|
|
298
|
-
// Add 1s buffer — step API uses second precision but log has sub-second timestamps
|
|
299
|
-
stepEndMs = new Date(failedStep.completed_at).getTime() + 999;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
catch { }
|
|
303
|
-
}
|
|
304
|
-
if (!stepStartMs)
|
|
305
|
-
return detail;
|
|
306
|
-
// Fetch job log via API (works even while run is still in progress)
|
|
307
|
-
const logOutput = run(`gh api repos/{owner}/{repo}/actions/jobs/${jobId}/logs 2>/dev/null`);
|
|
308
|
-
if (logOutput) {
|
|
309
|
-
const lines = logOutput.split("\n");
|
|
310
|
-
// Filter to lines within the failed step's time range
|
|
311
|
-
const stepLines = lines.filter((line) => {
|
|
312
|
-
const m = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z)/);
|
|
313
|
-
if (!m)
|
|
314
|
-
return false;
|
|
315
|
-
const ms = new Date(m[1]).getTime();
|
|
316
|
-
return ms >= stepStartMs && ms <= stepEndMs;
|
|
317
|
-
});
|
|
318
|
-
// Truncate at ##[error] — everything after is post-run cleanup noise
|
|
319
|
-
const errorIdx = stepLines.findIndex((l) => l.includes("##[error]"));
|
|
320
|
-
const bounded = errorIdx !== -1 ? stepLines.slice(0, errorIdx) : stepLines;
|
|
321
|
-
// Split non-group output into segments separated by ##[group]..##[endgroup] blocks.
|
|
322
|
-
// The last segment is the actual command output, earlier segments are
|
|
323
|
-
// setup noise (checkout, env vars, etc.).
|
|
324
|
-
const segments = [];
|
|
325
|
-
let current = [];
|
|
326
|
-
let inGroup = false;
|
|
327
|
-
for (const raw of bounded) {
|
|
328
|
-
const line = raw.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, "");
|
|
329
|
-
if (line.startsWith("##[group]")) {
|
|
330
|
-
if (current.length) {
|
|
331
|
-
segments.push(current);
|
|
332
|
-
current = [];
|
|
333
|
-
}
|
|
334
|
-
inGroup = true;
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (line.startsWith("##[endgroup]")) {
|
|
338
|
-
inGroup = false;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
if (line.startsWith("##["))
|
|
342
|
-
continue;
|
|
343
|
-
if (!inGroup)
|
|
344
|
-
current.push(line);
|
|
345
|
-
}
|
|
346
|
-
if (current.length)
|
|
347
|
-
segments.push(current);
|
|
348
|
-
if (segments.length)
|
|
349
|
-
detail.log = segments[segments.length - 1].join("\n");
|
|
350
|
-
}
|
|
351
|
-
return detail;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Fetch CI check results for a pull request.
|
|
355
|
-
* Runs: `gh pr checks <prNumber> --json name,state,bucket,link,description,workflow`
|
|
356
|
-
* Returns null if gh CLI fails.
|
|
357
|
-
*/
|
|
358
|
-
export function getPRChecks(prNumber) {
|
|
359
|
-
const output = run(`gh pr checks ${prNumber} --json name,state,bucket,link,description,workflow`);
|
|
360
|
-
if (!output)
|
|
361
|
-
return null;
|
|
362
|
-
try {
|
|
363
|
-
return JSON.parse(output);
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
return null;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Fetch reviews for a pull request.
|
|
371
|
-
* Runs: `gh pr view <prNumber> --json reviews`
|
|
372
|
-
* Returns null if gh CLI fails.
|
|
373
|
-
*/
|
|
374
|
-
export function getPRReviews(prNumber) {
|
|
375
|
-
const output = run(`gh pr view ${prNumber} --json reviews`);
|
|
376
|
-
if (!output)
|
|
377
|
-
return null;
|
|
378
|
-
try {
|
|
379
|
-
const data = JSON.parse(output);
|
|
380
|
-
return data.reviews ?? null;
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Fetch inline review comments for a pull request via the GitHub API.
|
|
388
|
-
* Runs: `gh api repos/{owner}/{repo}/pulls/<prNumber>/comments --paginate`
|
|
389
|
-
* Returns null if gh CLI fails.
|
|
390
|
-
*/
|
|
391
|
-
export function getPRReviewComments(prNumber) {
|
|
392
|
-
const output = run(`gh api repos/{owner}/{repo}/pulls/${prNumber}/comments --paginate`);
|
|
393
|
-
if (!output)
|
|
394
|
-
return null;
|
|
395
|
-
try {
|
|
396
|
-
return JSON.parse(output);
|
|
397
|
-
}
|
|
398
|
-
catch {
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
}
|
package/package.json
CHANGED
package/prompts/fix-pr.njk
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{% if ticket_content %}
|
|
2
2
|
{{ ticket_content }}
|
|
3
3
|
{% else %}
|
|
4
|
-
Note: Could not fetch Linear ticket {{ ticket_id }}
|
|
4
|
+
Note: Could not fetch Linear ticket {{ ticket_id }} directly.
|
|
5
|
+
If a Linear MCP server is available, use it to fetch the ticket description, comments, and any relevant details for {{ ticket_id }}.
|
|
6
|
+
Otherwise, proceed based on branch name context.
|
|
5
7
|
{% endif %}
|
|
6
8
|
{% if pr_feedback %}
|
|
7
9
|
{{ pr_feedback }}
|
package/prompts/review.njk
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{% if ticket_content %}
|
|
2
2
|
{{ ticket_content }}
|
|
3
3
|
{% else %}
|
|
4
|
-
Note: Could not fetch Linear ticket {{ ticket_id }}
|
|
4
|
+
Note: Could not fetch Linear ticket {{ ticket_id }} directly.
|
|
5
|
+
If a Linear MCP server is available, use it to fetch the ticket description, comments, and any relevant details for {{ ticket_id }}.
|
|
6
|
+
Otherwise, proceed based on branch name context.
|
|
5
7
|
{% endif %}
|
|
6
8
|
{% if diff_content %}
|
|
7
9
|
{{ diff_content }}
|
package/prompts/work.njk
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{% if ticket_content %}
|
|
2
2
|
{{ ticket_content }}
|
|
3
3
|
{% else %}
|
|
4
|
-
Note: Could not fetch Linear ticket {{ ticket_id }}
|
|
4
|
+
Note: Could not fetch Linear ticket {{ ticket_id }} directly.
|
|
5
|
+
If a Linear MCP server is available, use it to fetch the ticket description, comments, and any relevant details for {{ ticket_id }}.
|
|
6
|
+
Otherwise, proceed based on branch name context.
|
|
5
7
|
{% endif %}
|
|
6
8
|
|
|
7
9
|
Review the codebase to understand the relevant areas and existing patterns.
|