santree 0.0.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.
@@ -0,0 +1,169 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Text, Box } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { z } from "zod";
6
+ import { spawn } from "child_process";
7
+ import { getCurrentBranch, extractTicketId, findRepoRoot } from "../lib/git.js";
8
+ export const options = z.object({
9
+ plan: z.boolean().optional().describe("Only create implementation plan"),
10
+ review: z.boolean().optional().describe("Review changes against ticket"),
11
+ "fix-pr": z.boolean().optional().describe("Fetch PR comments and fix them"),
12
+ });
13
+ const TEMPLATES = {
14
+ implement: `Fetch Linear ticket {{ticket_id}} using MCP (including issue comments) and analyze what needs to be done.
15
+
16
+ If a PR URL is linked in the ticket, use \`gh\` CLI to fetch PR details, comments, and review feedback.
17
+
18
+ Review the codebase to understand the relevant areas and existing patterns.
19
+
20
+ Create an implementation plan, then implement the changes.
21
+
22
+ After implementation:
23
+ - Run tests if applicable
24
+ - Ensure code follows existing patterns`,
25
+ plan: `Fetch Linear ticket {{ticket_id}} using MCP (including issue comments) and analyze what needs to be done.
26
+
27
+ If a PR URL is linked in the ticket, use \`gh\` CLI to fetch PR details, comments, and review feedback for additional context.
28
+
29
+ Review the codebase to understand:
30
+ - Relevant files and modules
31
+ - Existing patterns and conventions
32
+ - Dependencies and potential impact areas
33
+
34
+ Create a detailed implementation plan with:
35
+ - Step-by-step approach
36
+ - Files to modify
37
+ - Potential risks or edge cases
38
+
39
+ Do NOT implement yet - just plan. Wait for approval before making changes.`,
40
+ review: `Fetch Linear ticket {{ticket_id}} using MCP (including issue comments) to understand the requirements and acceptance criteria.
41
+
42
+ If a PR URL is linked in the ticket, use \`gh\` CLI to fetch PR details and any existing review comments.
43
+
44
+ Review the current changes by running \`git diff\` against the base branch.
45
+
46
+ Analyze:
47
+ - Do the changes fully address the ticket requirements?
48
+ - Are there any missing acceptance criteria?
49
+ - Any potential bugs or edge cases?
50
+ - Code quality and adherence to patterns?
51
+
52
+ Provide a summary of findings and any recommended changes.`,
53
+ "fix-pr": `Fetch Linear ticket {{ticket_id}} using MCP (including issue comments) to understand the original requirements.
54
+
55
+ If a PR URL is linked in the ticket, use \`gh\` CLI to fetch the latest PR comments and review feedback.
56
+
57
+ ## PR Comments to Address
58
+
59
+ {{pr_comments}}
60
+
61
+ ## Task
62
+
63
+ 1. Review each comment and understand what changes are requested
64
+ 2. Make the necessary code changes to address each comment
65
+ 3. Ensure the changes align with the original ticket requirements
66
+ 4. Run tests if applicable to verify the fixes
67
+
68
+ Address all comments systematically, starting from the most critical ones.`,
69
+ };
70
+ function getMode(opts) {
71
+ if (opts["fix-pr"])
72
+ return "fix-pr";
73
+ if (opts.review)
74
+ return "review";
75
+ if (opts.plan)
76
+ return "plan";
77
+ return "implement";
78
+ }
79
+ function getModeLabel(mode) {
80
+ switch (mode) {
81
+ case "implement":
82
+ return "implement";
83
+ case "plan":
84
+ return "plan only";
85
+ case "review":
86
+ return "review";
87
+ case "fix-pr":
88
+ return "fix PR";
89
+ }
90
+ }
91
+ function getModeColor(mode) {
92
+ switch (mode) {
93
+ case "implement":
94
+ return "green";
95
+ case "plan":
96
+ return "blue";
97
+ case "review":
98
+ return "yellow";
99
+ case "fix-pr":
100
+ return "magenta";
101
+ }
102
+ }
103
+ export default function Work({ options }) {
104
+ const [status, setStatus] = useState("loading");
105
+ const [branch, setBranch] = useState(null);
106
+ const [ticketId, setTicketId] = useState(null);
107
+ const [error, setError] = useState(null);
108
+ const [mode, setMode] = useState("implement");
109
+ useEffect(() => {
110
+ async function init() {
111
+ // Small delay to allow spinner to render
112
+ await new Promise((r) => setTimeout(r, 100));
113
+ const repoRoot = findRepoRoot();
114
+ if (!repoRoot) {
115
+ setStatus("error");
116
+ setError("Not inside a git repository");
117
+ return;
118
+ }
119
+ const currentBranch = getCurrentBranch();
120
+ if (!currentBranch) {
121
+ setStatus("error");
122
+ setError("Could not determine current branch");
123
+ return;
124
+ }
125
+ setBranch(currentBranch);
126
+ const ticket = extractTicketId(currentBranch);
127
+ if (!ticket) {
128
+ setStatus("error");
129
+ setError("Could not extract ticket ID from branch name. Expected format: user/TEAM-123-description");
130
+ return;
131
+ }
132
+ setTicketId(ticket);
133
+ setMode(getMode(options));
134
+ setStatus("ready");
135
+ }
136
+ init();
137
+ }, [options]);
138
+ useEffect(() => {
139
+ if (status !== "ready" || !ticketId)
140
+ return;
141
+ setStatus("launching");
142
+ const template = TEMPLATES[mode];
143
+ const prompt = template.replace(/\{\{ticket_id\}\}/g, ticketId);
144
+ const happyCmd = "happy";
145
+ // Build args array
146
+ const args = [];
147
+ // Allow Linear MCP tools without prompting
148
+ args.push("--allowedTools", "mcp__linear__list_comments mcp__linear__get_issue");
149
+ // Add plan mode flag (Claude's native plan mode)
150
+ if (mode === "plan") {
151
+ args.push("--permission-mode", "plan");
152
+ }
153
+ // Add the prompt
154
+ args.push(prompt);
155
+ // Spawn happy directly with prompt as argument (no shell)
156
+ const child = spawn(happyCmd, args, {
157
+ stdio: "inherit",
158
+ });
159
+ child.on("error", (err) => {
160
+ setStatus("error");
161
+ setError(`Failed to launch happy: ${err.message}`);
162
+ });
163
+ child.on("close", () => {
164
+ process.exit(0);
165
+ });
166
+ }, [status, ticketId, mode]);
167
+ const isLoading = status === "loading";
168
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83E\uDD16 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: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" ", "happy", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
169
+ }
@@ -0,0 +1,47 @@
1
+ export interface Worktree {
2
+ path: string;
3
+ branch: string;
4
+ commit: string;
5
+ isBare: boolean;
6
+ }
7
+ export declare function findRepoRoot(): string | null;
8
+ export declare function findMainRepoRoot(): string | null;
9
+ export declare function isInWorktree(): boolean;
10
+ export declare function isWorktreePath(wtPath: string): boolean;
11
+ export declare function getCurrentBranch(): string | null;
12
+ export declare function getDefaultBranch(): string;
13
+ export declare function listWorktrees(): Worktree[];
14
+ export declare function getSantreeDir(repoRoot: string): string;
15
+ export declare function getWorktreesDir(repoRoot: string): string;
16
+ export declare function createWorktree(branchName: string, baseBranch: string, repoRoot: string): Promise<{
17
+ success: boolean;
18
+ path?: string;
19
+ error?: string;
20
+ }>;
21
+ export declare function removeWorktree(branchName: string, repoRoot: string, force?: boolean): Promise<{
22
+ success: boolean;
23
+ error?: string;
24
+ }>;
25
+ export declare function extractTicketId(branch: string): string | null;
26
+ export declare function getWorktreePath(branchName: string): string | null;
27
+ export declare function getWorktreeMetadata(worktreePath: string): {
28
+ branch_name?: string;
29
+ base_branch?: string;
30
+ created_at?: string;
31
+ } | null;
32
+ export declare function hasUncommittedChanges(): boolean;
33
+ export declare function hasStagedChanges(): boolean;
34
+ export declare function hasUnstagedChanges(): boolean;
35
+ export declare function getGitStatus(): string;
36
+ export declare function getStagedDiffStat(): string;
37
+ export declare function getCommitsBehind(baseBranch: string): number;
38
+ export declare function getCommitsAhead(baseBranch: string): number;
39
+ export declare function remoteBranchExists(branchName: string): boolean;
40
+ export declare function getUnpushedCommits(branchName: string): number;
41
+ export declare function pullLatest(baseBranch: string, repoRoot: string): {
42
+ success: boolean;
43
+ message: string;
44
+ };
45
+ export declare function hasInitScript(repoRoot: string): boolean;
46
+ export declare function getInitScriptPath(repoRoot: string): string;
47
+ export declare function getLatestCommitMessage(): string | null;
@@ -0,0 +1,393 @@
1
+ import { execSync, exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ const execAsync = promisify(exec);
6
+ export function findRepoRoot() {
7
+ try {
8
+ return execSync("git rev-parse --show-toplevel", {
9
+ encoding: "utf-8",
10
+ }).trim();
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function findMainRepoRoot() {
17
+ try {
18
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
19
+ encoding: "utf-8",
20
+ }).trim();
21
+ return path.dirname(path.resolve(gitCommonDir));
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function isInWorktree() {
28
+ try {
29
+ const gitDir = execSync("git rev-parse --git-dir", {
30
+ encoding: "utf-8",
31
+ }).trim();
32
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
33
+ encoding: "utf-8",
34
+ }).trim();
35
+ // If they differ, we're in a worktree
36
+ return path.resolve(gitDir) !== path.resolve(gitCommonDir);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ export function isWorktreePath(wtPath) {
43
+ try {
44
+ const gitDir = execSync("git rev-parse --git-dir", {
45
+ encoding: "utf-8",
46
+ cwd: wtPath,
47
+ }).trim();
48
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
49
+ encoding: "utf-8",
50
+ cwd: wtPath,
51
+ }).trim();
52
+ // If they differ, it's a worktree (not the main repo)
53
+ return path.resolve(wtPath, gitDir) !== path.resolve(wtPath, gitCommonDir);
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ export function getCurrentBranch() {
60
+ try {
61
+ return execSync("git rev-parse --abbrev-ref HEAD", {
62
+ encoding: "utf-8",
63
+ }).trim();
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ export function getDefaultBranch() {
70
+ try {
71
+ const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
72
+ encoding: "utf-8",
73
+ }).trim();
74
+ return ref.replace("refs/remotes/origin/", "");
75
+ }
76
+ catch {
77
+ // Fall back to checking if main/master exists
78
+ for (const branch of ["main", "master"]) {
79
+ try {
80
+ execSync(`git rev-parse --verify refs/heads/${branch}`, {
81
+ stdio: "ignore",
82
+ });
83
+ return branch;
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ }
89
+ return "main";
90
+ }
91
+ }
92
+ export function listWorktrees() {
93
+ try {
94
+ const output = execSync("git worktree list --porcelain", {
95
+ encoding: "utf-8",
96
+ });
97
+ const worktrees = [];
98
+ let current = {};
99
+ for (const line of output.split("\n")) {
100
+ if (line.startsWith("worktree ")) {
101
+ current.path = line.replace("worktree ", "");
102
+ }
103
+ else if (line.startsWith("HEAD ")) {
104
+ current.commit = line.replace("HEAD ", "").slice(0, 8);
105
+ }
106
+ else if (line.startsWith("branch ")) {
107
+ current.branch = line.replace("branch refs/heads/", "");
108
+ }
109
+ else if (line === "bare") {
110
+ current.isBare = true;
111
+ }
112
+ else if (line === "" && current.path) {
113
+ worktrees.push(current);
114
+ current = {};
115
+ }
116
+ }
117
+ if (current.path) {
118
+ worktrees.push(current);
119
+ }
120
+ return worktrees;
121
+ }
122
+ catch {
123
+ return [];
124
+ }
125
+ }
126
+ export function getSantreeDir(repoRoot) {
127
+ return path.join(repoRoot, ".santree");
128
+ }
129
+ export function getWorktreesDir(repoRoot) {
130
+ return path.join(getSantreeDir(repoRoot), "worktrees");
131
+ }
132
+ export async function createWorktree(branchName, baseBranch, repoRoot) {
133
+ const dirName = branchName.replace(/\//g, "__");
134
+ const worktreesDir = getWorktreesDir(repoRoot);
135
+ const worktreePath = path.join(worktreesDir, dirName);
136
+ if (fs.existsSync(worktreePath)) {
137
+ return {
138
+ success: false,
139
+ error: `Worktree already exists at ${worktreePath}`,
140
+ };
141
+ }
142
+ // Ensure worktrees directory exists
143
+ fs.mkdirSync(worktreesDir, { recursive: true });
144
+ // Check if branch exists
145
+ let branchExists = false;
146
+ try {
147
+ execSync(`git rev-parse --verify refs/heads/${branchName}`, {
148
+ cwd: repoRoot,
149
+ stdio: "ignore",
150
+ });
151
+ branchExists = true;
152
+ }
153
+ catch {
154
+ branchExists = false;
155
+ }
156
+ try {
157
+ if (branchExists) {
158
+ await execAsync(`git worktree add "${worktreePath}" "${branchName}"`, {
159
+ cwd: repoRoot,
160
+ });
161
+ }
162
+ else {
163
+ await execAsync(`git worktree add -b "${branchName}" "${worktreePath}" "${baseBranch}"`, { cwd: repoRoot });
164
+ }
165
+ // Save metadata
166
+ const metadata = {
167
+ branch_name: branchName,
168
+ base_branch: baseBranch,
169
+ created_at: new Date().toISOString(),
170
+ };
171
+ fs.writeFileSync(path.join(worktreePath, ".santree_metadata.json"), JSON.stringify(metadata, null, 2));
172
+ return { success: true, path: worktreePath };
173
+ }
174
+ catch (e) {
175
+ return {
176
+ success: false,
177
+ error: e instanceof Error ? e.message : "Unknown error",
178
+ };
179
+ }
180
+ }
181
+ export async function removeWorktree(branchName, repoRoot, force = false) {
182
+ // Find the worktree by branch name using git's worktree tracking
183
+ const worktreePath = getWorktreePath(branchName);
184
+ if (!worktreePath) {
185
+ return { success: false, error: `Worktree not found: ${branchName}` };
186
+ }
187
+ try {
188
+ const forceFlag = force ? "--force" : "";
189
+ await execAsync(`git worktree remove ${forceFlag} "${worktreePath}"`, {
190
+ cwd: repoRoot,
191
+ });
192
+ // Clean up any remaining files (untracked files, node_modules, etc.)
193
+ // git worktree remove doesn't delete untracked files
194
+ if (fs.existsSync(worktreePath)) {
195
+ // Fix permissions first (node_modules often has restricted perms)
196
+ try {
197
+ execSync(`chmod -R u+w "${worktreePath}"`, { stdio: "ignore" });
198
+ }
199
+ catch {
200
+ // Ignore chmod errors
201
+ }
202
+ fs.rmSync(worktreePath, { recursive: true, force: true });
203
+ }
204
+ // Also delete the branch
205
+ const deleteFlag = force ? "-D" : "-d";
206
+ try {
207
+ await execAsync(`git branch ${deleteFlag} "${branchName}"`, {
208
+ cwd: repoRoot,
209
+ });
210
+ }
211
+ catch {
212
+ // Branch deletion failed, but worktree was removed
213
+ }
214
+ return { success: true };
215
+ }
216
+ catch (e) {
217
+ return {
218
+ success: false,
219
+ error: e instanceof Error ? e.message : "Unknown error",
220
+ };
221
+ }
222
+ }
223
+ export function extractTicketId(branch) {
224
+ const match = branch.match(/([a-zA-Z]+)-(\d+)/);
225
+ if (match) {
226
+ return `${match[1].toUpperCase()}-${match[2]}`;
227
+ }
228
+ return null;
229
+ }
230
+ export function getWorktreePath(branchName) {
231
+ const worktrees = listWorktrees();
232
+ const wt = worktrees.find((w) => w.branch === branchName);
233
+ return wt?.path ?? null;
234
+ }
235
+ export function getWorktreeMetadata(worktreePath) {
236
+ const metadataPath = path.join(worktreePath, ".santree_metadata.json");
237
+ if (!fs.existsSync(metadataPath)) {
238
+ return null;
239
+ }
240
+ try {
241
+ return JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }
247
+ export function hasUncommittedChanges() {
248
+ try {
249
+ const output = execSync("git status --porcelain", { encoding: "utf-8" });
250
+ return Boolean(output.trim());
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ }
256
+ export function hasStagedChanges() {
257
+ try {
258
+ execSync("git diff --cached --quiet", { stdio: "ignore" });
259
+ return false;
260
+ }
261
+ catch {
262
+ return true;
263
+ }
264
+ }
265
+ export function hasUnstagedChanges() {
266
+ try {
267
+ // Check for modified files
268
+ try {
269
+ execSync("git diff --quiet", { stdio: "ignore" });
270
+ }
271
+ catch {
272
+ return true;
273
+ }
274
+ // Check for untracked files
275
+ const output = execSync("git ls-files --others --exclude-standard", {
276
+ encoding: "utf-8",
277
+ });
278
+ return Boolean(output.trim());
279
+ }
280
+ catch {
281
+ return false;
282
+ }
283
+ }
284
+ export function getGitStatus() {
285
+ try {
286
+ return execSync("git status --short", { encoding: "utf-8" }).trim();
287
+ }
288
+ catch {
289
+ return "";
290
+ }
291
+ }
292
+ export function getStagedDiffStat() {
293
+ try {
294
+ return execSync("git diff --cached --stat", { encoding: "utf-8" }).trim();
295
+ }
296
+ catch {
297
+ return "";
298
+ }
299
+ }
300
+ export function getCommitsBehind(baseBranch) {
301
+ try {
302
+ const output = execSync(`git rev-list --count HEAD..origin/${baseBranch}`, {
303
+ encoding: "utf-8",
304
+ });
305
+ return parseInt(output.trim(), 10) || 0;
306
+ }
307
+ catch {
308
+ return 0;
309
+ }
310
+ }
311
+ export function getCommitsAhead(baseBranch) {
312
+ try {
313
+ const output = execSync(`git rev-list --count ${baseBranch}..HEAD`, {
314
+ encoding: "utf-8",
315
+ });
316
+ return parseInt(output.trim(), 10) || 0;
317
+ }
318
+ catch {
319
+ return 0;
320
+ }
321
+ }
322
+ export function remoteBranchExists(branchName) {
323
+ try {
324
+ const output = execSync(`git ls-remote --heads origin ${branchName}`, {
325
+ encoding: "utf-8",
326
+ });
327
+ return output.includes(branchName);
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ }
333
+ export function getUnpushedCommits(branchName) {
334
+ try {
335
+ // Check if remote tracking branch exists
336
+ try {
337
+ execSync(`git rev-parse --verify origin/${branchName}`, {
338
+ stdio: "ignore",
339
+ });
340
+ }
341
+ catch {
342
+ // No remote branch, count all local commits
343
+ const output = execSync("git rev-list --count HEAD", {
344
+ encoding: "utf-8",
345
+ });
346
+ return parseInt(output.trim(), 10) || 0;
347
+ }
348
+ // Count commits ahead of remote
349
+ const output = execSync(`git rev-list --count origin/${branchName}..HEAD`, {
350
+ encoding: "utf-8",
351
+ });
352
+ return parseInt(output.trim(), 10) || 0;
353
+ }
354
+ catch {
355
+ return 0;
356
+ }
357
+ }
358
+ export function pullLatest(baseBranch, repoRoot) {
359
+ try {
360
+ // Fetch from origin
361
+ execSync("git fetch origin", { cwd: repoRoot, stdio: "ignore" });
362
+ // Update the base branch
363
+ execSync(`git checkout ${baseBranch}`, { cwd: repoRoot, stdio: "ignore" });
364
+ execSync(`git pull origin ${baseBranch}`, {
365
+ cwd: repoRoot,
366
+ stdio: "ignore",
367
+ });
368
+ return { success: true, message: "Fetched latest changes" };
369
+ }
370
+ catch (e) {
371
+ return {
372
+ success: false,
373
+ message: e instanceof Error ? e.message : "Failed to pull latest",
374
+ };
375
+ }
376
+ }
377
+ export function hasInitScript(repoRoot) {
378
+ const initScript = path.join(getSantreeDir(repoRoot), "init.sh");
379
+ return fs.existsSync(initScript);
380
+ }
381
+ export function getInitScriptPath(repoRoot) {
382
+ return path.join(getSantreeDir(repoRoot), "init.sh");
383
+ }
384
+ export function getLatestCommitMessage() {
385
+ try {
386
+ return execSync("git log -1 --format=%s", {
387
+ encoding: "utf-8",
388
+ }).trim();
389
+ }
390
+ catch {
391
+ return null;
392
+ }
393
+ }
@@ -0,0 +1,11 @@
1
+ export interface PRInfo {
2
+ number: string;
3
+ state: "OPEN" | "MERGED" | "CLOSED";
4
+ url?: string;
5
+ }
6
+ export declare function getPRInfo(branchName: string): PRInfo | null;
7
+ export declare function getPRInfoAsync(branchName: string): Promise<PRInfo | null>;
8
+ export declare function ghCliAvailable(): boolean;
9
+ export declare function pushBranch(branchName: string, force?: boolean): boolean;
10
+ export declare function createPR(title: string, baseBranch: string, headBranch: string, draft: boolean): number;
11
+ export declare function getPRComments(prNumber: string): string;
@@ -0,0 +1,71 @@
1
+ import { execSync, exec } from "child_process";
2
+ import { promisify } from "util";
3
+ const execAsync = promisify(exec);
4
+ export function getPRInfo(branchName) {
5
+ try {
6
+ const output = execSync(`gh pr view "${branchName}" --json number,state,url`, { encoding: "utf-8" });
7
+ const data = JSON.parse(output);
8
+ return {
9
+ number: String(data.number ?? ""),
10
+ state: data.state ?? "OPEN",
11
+ url: data.url,
12
+ };
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export async function getPRInfoAsync(branchName) {
19
+ try {
20
+ const { stdout } = await execAsync(`gh pr view "${branchName}" --json number,state,url`);
21
+ const data = JSON.parse(stdout);
22
+ return {
23
+ number: String(data.number ?? ""),
24
+ state: data.state ?? "OPEN",
25
+ url: data.url,
26
+ };
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export function ghCliAvailable() {
33
+ try {
34
+ execSync("which gh", { stdio: "ignore" });
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ export function pushBranch(branchName, force = false) {
42
+ try {
43
+ const forceFlag = force ? "--force-with-lease" : "";
44
+ execSync(`git push -u origin "${branchName}" ${forceFlag}`.trim(), {
45
+ stdio: "inherit",
46
+ });
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ export function createPR(title, baseBranch, headBranch, draft) {
54
+ try {
55
+ const draftFlag = draft ? "--draft" : "";
56
+ execSync(`gh pr create --title "${title}" --base "${baseBranch}" --head "${headBranch}" --web ${draftFlag}`.trim(), { stdio: "inherit" });
57
+ return 0;
58
+ }
59
+ catch {
60
+ return 1;
61
+ }
62
+ }
63
+ export function getPRComments(prNumber) {
64
+ try {
65
+ const output = execSync(`gh pr view ${prNumber} --json comments --jq '.comments[] | "- \\(.author.login): \\(.body)"'`, { encoding: "utf-8" });
66
+ return output.trim();
67
+ }
68
+ catch {
69
+ return "";
70
+ }
71
+ }