stella-coder 5.1.2 → 5.2.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/package.json +2 -1
- package/stella-cli/browser-control.mjs +274 -0
- package/stella-cli/git-api.mjs +407 -0
- package/stella-cli/index.mjs +687 -1
- package/stella-cli/screen-monitor.mjs +334 -0
- package/stella-cli/web-parser.mjs +229 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stella-coder",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
|
|
6
6
|
"main": "stella-cli/index.mjs",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"class-variance-authority": "^0.7.1",
|
|
37
37
|
"clsx": "^2.1.1",
|
|
38
38
|
"gsap": "^3.15.0",
|
|
39
|
+
"jsdom": "^29.1.1",
|
|
39
40
|
"lucide-react": "^1.16.0",
|
|
40
41
|
"next": "16.2.6",
|
|
41
42
|
"react": "^19",
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { execSync, spawn } from "child_process"
|
|
2
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { homedir } from "os"
|
|
5
|
+
import http from "http"
|
|
6
|
+
|
|
7
|
+
const STELLA_DIR = join(homedir(), ".stella")
|
|
8
|
+
const BROWSER_DIR = join(STELLA_DIR, "browser")
|
|
9
|
+
const PROFILES_DIR = join(BROWSER_DIR, "profiles")
|
|
10
|
+
|
|
11
|
+
function ensureDir() {
|
|
12
|
+
if (!existsSync(BROWSER_DIR)) mkdirSync(BROWSER_DIR, { recursive: true })
|
|
13
|
+
if (!existsSync(PROFILES_DIR)) mkdirSync(PROFILES_DIR, { recursive: true })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function httpGet(url) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
http.get(url, (res) => {
|
|
19
|
+
let data = ""
|
|
20
|
+
res.on("data", (chunk) => data += chunk)
|
|
21
|
+
res.on("end", () => {
|
|
22
|
+
try { resolve(JSON.parse(data)) } catch { resolve(data) }
|
|
23
|
+
})
|
|
24
|
+
}).on("error", reject)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ChromeBrowser {
|
|
29
|
+
constructor() {
|
|
30
|
+
ensureDir()
|
|
31
|
+
this.ws = null
|
|
32
|
+
this.pageId = null
|
|
33
|
+
this.debugPort = 9222
|
|
34
|
+
this.proc = null
|
|
35
|
+
this.msgId = 0
|
|
36
|
+
this.callbacks = new Map()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async launch(headless = false) {
|
|
40
|
+
const chromePaths = [
|
|
41
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
42
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
43
|
+
join(homedir(), "AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"),
|
|
44
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
45
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
let chromePath = null
|
|
49
|
+
for (const p of chromePaths) {
|
|
50
|
+
if (existsSync(p)) { chromePath = p; break }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!chromePath) {
|
|
54
|
+
return { success: false, error: "Chrome/Edge not found" }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const args = [
|
|
58
|
+
`--remote-debugging-port=${this.debugPort}`,
|
|
59
|
+
"--no-first-run",
|
|
60
|
+
"--no-default-browser-check",
|
|
61
|
+
headless ? "--headless=new" : "",
|
|
62
|
+
`--user-data-dir=${join(PROFILES_DIR, "default")}`,
|
|
63
|
+
"about:blank",
|
|
64
|
+
].filter(Boolean)
|
|
65
|
+
|
|
66
|
+
this.proc = spawn(chromePath, args, { detached: true, stdio: "ignore" })
|
|
67
|
+
this.proc.unref()
|
|
68
|
+
|
|
69
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
|
|
73
|
+
if (tabs && tabs.length > 0) {
|
|
74
|
+
this.pageId = tabs[0].id
|
|
75
|
+
return { success: true, port: this.debugPort, pid: this.proc.pid }
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { success: false, error: `Chrome launched but DevTools not ready: ${e.message}` }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { success: true, port: this.debugPort }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async connect() {
|
|
85
|
+
try {
|
|
86
|
+
const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
|
|
87
|
+
if (!tabs || tabs.length === 0) {
|
|
88
|
+
return { success: false, error: "No tabs found. Launch Chrome first." }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const wsUrl = tabs[0].webSocketDebuggerUrl
|
|
92
|
+
if (!wsUrl) {
|
|
93
|
+
return { success: false, error: "WebSocket URL not available" }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
this.ws = new WebSocket(wsUrl)
|
|
98
|
+
this.ws.onopen = () => {
|
|
99
|
+
this.pageId = tabs[0].id
|
|
100
|
+
resolve({ success: true, title: tabs[0].title })
|
|
101
|
+
}
|
|
102
|
+
this.ws.onmessage = (event) => {
|
|
103
|
+
const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString())
|
|
104
|
+
if (msg.id && this.callbacks.has(msg.id)) {
|
|
105
|
+
this.callbacks.get(msg.id)(msg)
|
|
106
|
+
this.callbacks.delete(msg.id)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.ws.onerror = (err) => {
|
|
110
|
+
resolve({ success: false, error: err.message || "WebSocket error" })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { success: false, error: e.message }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async sendCommand(method, params = {}) {
|
|
119
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
120
|
+
const conn = await this.connect()
|
|
121
|
+
if (!conn.success) return { success: false, error: conn.error }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const id = ++this.msgId
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
this.callbacks.set(id, (msg) => {
|
|
127
|
+
if (msg.error) {
|
|
128
|
+
resolve({ success: false, error: msg.error.message })
|
|
129
|
+
} else {
|
|
130
|
+
resolve({ success: true, result: msg.result })
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
this.ws.send(JSON.stringify({ id, method, params }))
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (this.callbacks.has(id)) {
|
|
136
|
+
this.callbacks.delete(id)
|
|
137
|
+
resolve({ success: false, error: "Command timeout" })
|
|
138
|
+
}
|
|
139
|
+
}, 30000)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async navigate(url) {
|
|
144
|
+
const result = await this.sendCommand("Page.navigate", { url })
|
|
145
|
+
if (result.success) await new Promise(r => setTimeout(r, 2000))
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getPageContent() {
|
|
150
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
151
|
+
expression: "document.documentElement.outerHTML",
|
|
152
|
+
returnByValue: true,
|
|
153
|
+
})
|
|
154
|
+
return result.success ? { success: true, html: result.result?.value || "" } : result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getText() {
|
|
158
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
159
|
+
expression: "document.body.innerText",
|
|
160
|
+
returnByValue: true,
|
|
161
|
+
})
|
|
162
|
+
return result.success ? { success: true, text: result.result?.value || "" } : result
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getTitle() {
|
|
166
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
167
|
+
expression: "document.title",
|
|
168
|
+
returnByValue: true,
|
|
169
|
+
})
|
|
170
|
+
return result.success ? { success: true, title: result.result?.value || "" } : result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async screenshot(path) {
|
|
174
|
+
const result = await this.sendCommand("Page.captureScreenshot", { format: "png" })
|
|
175
|
+
if (result.success && result.result?.data) {
|
|
176
|
+
const imgPath = path || join(BROWSER_DIR, `screenshot_${Date.now()}.png`)
|
|
177
|
+
writeFileSync(imgPath, Buffer.from(result.result.data, "base64"))
|
|
178
|
+
return { success: true, path: imgPath }
|
|
179
|
+
}
|
|
180
|
+
return result
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async evaluate(expression) {
|
|
184
|
+
return await this.sendCommand("Runtime.evaluate", {
|
|
185
|
+
expression,
|
|
186
|
+
returnByValue: true,
|
|
187
|
+
awaitPromise: true,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async click(selector) {
|
|
192
|
+
const posResult = await this.sendCommand("Runtime.evaluate", {
|
|
193
|
+
expression: `(() => { const el = document.querySelector('${selector.replace(/'/g, "\\'")}'); if (!el) return null; const r = el.getBoundingClientRect(); return { x: r.x + r.width/2, y: r.y + r.height/2 }; })()`,
|
|
194
|
+
returnByValue: true,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if (!posResult.success || !posResult.result?.value) {
|
|
198
|
+
return { success: false, error: `Element not found: ${selector}` }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { x, y } = posResult.result.value
|
|
202
|
+
await this.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 })
|
|
203
|
+
await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 })
|
|
204
|
+
return { success: true, x, y }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async type(selector, text) {
|
|
208
|
+
await this.sendCommand("Runtime.evaluate", {
|
|
209
|
+
expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')?.focus()`,
|
|
210
|
+
})
|
|
211
|
+
for (const char of text) {
|
|
212
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", text: char, key: char })
|
|
213
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", key: char })
|
|
214
|
+
}
|
|
215
|
+
return { success: true }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async scroll(deltaX = 0, deltaY = 300) {
|
|
219
|
+
await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseWheel", x: 400, y: 400, deltaX, deltaY })
|
|
220
|
+
return { success: true }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async pressKey(key) {
|
|
224
|
+
const keys = {
|
|
225
|
+
enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
|
|
226
|
+
tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
|
|
227
|
+
escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
228
|
+
backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
|
|
229
|
+
space: { key: " ", code: "Space", windowsVirtualKeyCode: 32 },
|
|
230
|
+
arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
|
|
231
|
+
arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
|
|
232
|
+
}
|
|
233
|
+
const k = keys[key.toLowerCase()] || { key, code: `Key${key.toUpperCase()}` }
|
|
234
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", ...k })
|
|
235
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", ...k })
|
|
236
|
+
return { success: true }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async fillForm(fields) {
|
|
240
|
+
const results = []
|
|
241
|
+
for (const { selector, value } of fields) {
|
|
242
|
+
results.push({ selector, ...(await this.type(selector, value)) })
|
|
243
|
+
}
|
|
244
|
+
return { success: true, results }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async getLinks() {
|
|
248
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
249
|
+
expression: `Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim().slice(0, 100), href: a.href })).filter(a => a.text).slice(0, 50)`,
|
|
250
|
+
returnByValue: true,
|
|
251
|
+
})
|
|
252
|
+
return result.success ? { success: true, links: result.result?.value || [] } : result
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getForms() {
|
|
256
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
257
|
+
expression: `Array.from(document.querySelectorAll('form')).map(f => ({ action: f.action, method: f.method, inputs: Array.from(f.querySelectorAll('input,textarea,select')).map(i => ({ name: i.name, type: i.type, placeholder: i.placeholder })) }))`,
|
|
258
|
+
returnByValue: true,
|
|
259
|
+
})
|
|
260
|
+
return result.success ? { success: true, forms: result.result?.value || [] } : result
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
close() {
|
|
264
|
+
if (this.ws) this.ws.close()
|
|
265
|
+
if (this.proc) this.proc.kill()
|
|
266
|
+
return { success: true }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static async listTabs(port = 9222) {
|
|
270
|
+
try { return await httpGet(`http://127.0.0.1:${port}/json`) } catch { return [] }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export default ChromeBrowser
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { execSync } from "child_process"
|
|
2
|
+
import { readFileSync, existsSync } from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import http from "http"
|
|
5
|
+
import https from "https"
|
|
6
|
+
|
|
7
|
+
function apiRequest(url, options = {}) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const mod = url.startsWith("https") ? https : http
|
|
10
|
+
const urlObj = new URL(url)
|
|
11
|
+
const headers = {
|
|
12
|
+
"Accept": "application/vnd.github+json",
|
|
13
|
+
...options.headers,
|
|
14
|
+
}
|
|
15
|
+
if (options.token) headers["Authorization"] = `Bearer ${options.token}`
|
|
16
|
+
|
|
17
|
+
const reqOptions = {
|
|
18
|
+
hostname: urlObj.hostname,
|
|
19
|
+
port: urlObj.port,
|
|
20
|
+
path: urlObj.pathname + urlObj.search,
|
|
21
|
+
method: options.method || "GET",
|
|
22
|
+
headers,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const req = mod.request(reqOptions, (res) => {
|
|
26
|
+
let data = ""
|
|
27
|
+
res.on("data", (chunk) => data += chunk)
|
|
28
|
+
res.on("end", () => {
|
|
29
|
+
try { resolve({ status: res.statusCode, data: JSON.parse(data) }) }
|
|
30
|
+
catch { resolve({ status: res.statusCode, data }) }
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
req.on("error", reject)
|
|
34
|
+
if (options.body) req.write(JSON.stringify(options.body))
|
|
35
|
+
req.end()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getGitRemote() {
|
|
40
|
+
try {
|
|
41
|
+
const remote = execSync("git remote get-url origin", { encoding: "utf-8" }).trim()
|
|
42
|
+
// Parse owner/repo from URL
|
|
43
|
+
const match = remote.match(/github\.com[/:]([^/]+)\/([^/.]+)/)
|
|
44
|
+
if (match) return { provider: "github", owner: match[1], repo: match[2] }
|
|
45
|
+
const glMatch = remote.match(/gitlab\.com[/:]([^/]+)\/([^/.]+)/)
|
|
46
|
+
if (glMatch) return { provider: "gitlab", owner: glMatch[1], repo: glMatch[2] }
|
|
47
|
+
return { provider: "unknown", remote }
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getToken(provider) {
|
|
54
|
+
// Try environment variables
|
|
55
|
+
if (provider === "github") return process.env.GITHUB_TOKEN || process.env.GH_TOKEN
|
|
56
|
+
if (provider === "gitlab") return process.env.GITLAB_TOKEN || process.env.GL_TOKEN
|
|
57
|
+
|
|
58
|
+
// Try git credential
|
|
59
|
+
try {
|
|
60
|
+
const config = execSync("git config --get credential.helper", { encoding: "utf-8" }).trim()
|
|
61
|
+
if (config.includes("store")) {
|
|
62
|
+
const credPath = join(process.env.USERPROFILE || process.env.HOME || "", ".git-credentials")
|
|
63
|
+
if (existsSync(credPath)) {
|
|
64
|
+
const creds = readFileSync(credPath, "utf-8")
|
|
65
|
+
const match = creds.match(/https:\/\/[^:]+:([^@]+)@github\.com/)
|
|
66
|
+
if (match) return match[1]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class GitAPI {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.info = getGitRemote()
|
|
77
|
+
this.token = this.info ? getToken(this.info.provider) : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
isConfigured() {
|
|
81
|
+
return this.info && this.info.provider !== "unknown"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
hasToken() {
|
|
85
|
+
return !!this.token
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ===== ISSUES =====
|
|
89
|
+
async listIssues(state = "open", perPage = 20) {
|
|
90
|
+
if (!this.isConfigured()) return { success: false, error: "No GitHub/GitLab remote configured" }
|
|
91
|
+
const { provider, owner, repo } = this.info
|
|
92
|
+
const token = this.token
|
|
93
|
+
|
|
94
|
+
if (provider === "github") {
|
|
95
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${perPage}`
|
|
96
|
+
const res = await apiRequest(url, { token: this.token })
|
|
97
|
+
return { success: true, issues: Array.isArray(res.data) ? res.data : [] }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (provider === "gitlab") {
|
|
101
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
102
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/issues?state=${state}`
|
|
103
|
+
const res = await apiRequest(url, { token: this.token })
|
|
104
|
+
return { success: true, issues: Array.isArray(res.data) ? res.data : [] }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: false, error: "Unknown provider" }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async createIssue(title, body, labels = []) {
|
|
111
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
112
|
+
|
|
113
|
+
const { provider, owner, repo } = this.info
|
|
114
|
+
|
|
115
|
+
if (provider === "github") {
|
|
116
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues`
|
|
117
|
+
const res = await apiRequest(url, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
token: this.token,
|
|
120
|
+
body: { title, body, labels },
|
|
121
|
+
})
|
|
122
|
+
return { success: res.status === 201, data: res.data, url: res.data?.html_url }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (provider === "gitlab") {
|
|
126
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
127
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/issues`
|
|
128
|
+
const res = await apiRequest(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
token: this.token,
|
|
131
|
+
body: { title, description: body, labels: labels.join(",") },
|
|
132
|
+
})
|
|
133
|
+
return { success: res.status === 201, data: res.data, url: res.data?.web_url }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { success: false, error: "Unknown provider" }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async closeIssue(issueNumber) {
|
|
140
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
141
|
+
|
|
142
|
+
const { provider, owner, repo } = this.info
|
|
143
|
+
|
|
144
|
+
if (provider === "github") {
|
|
145
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`
|
|
146
|
+
const res = await apiRequest(url, {
|
|
147
|
+
method: "PATCH",
|
|
148
|
+
token: this.token,
|
|
149
|
+
body: { state: "closed" },
|
|
150
|
+
})
|
|
151
|
+
return { success: res.status === 200, data: res.data }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (provider === "gitlab") {
|
|
155
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
156
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/issues/${issueNumber}`
|
|
157
|
+
const res = await apiRequest(url, {
|
|
158
|
+
method: "PUT",
|
|
159
|
+
token: this.token,
|
|
160
|
+
body: { state_event: "close" },
|
|
161
|
+
})
|
|
162
|
+
return { success: res.status === 200, data: res.data }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { success: false, error: "Unknown provider" }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ===== PULL REQUESTS / MERGE REQUESTS =====
|
|
169
|
+
async listPRs(state = "open") {
|
|
170
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
171
|
+
|
|
172
|
+
const { provider, owner, repo } = this.info
|
|
173
|
+
|
|
174
|
+
if (provider === "github") {
|
|
175
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=${state}`
|
|
176
|
+
const res = await apiRequest(url, { token: this.token })
|
|
177
|
+
return { success: true, prs: Array.isArray(res.data) ? res.data : [] }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (provider === "gitlab") {
|
|
181
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
182
|
+
const stateMap = { open: "opened", closed: "closed", merged: "merged" }
|
|
183
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests?state=${stateMap[state] || state}`
|
|
184
|
+
const res = await apiRequest(url, { token: this.token })
|
|
185
|
+
return { success: true, prs: Array.isArray(res.data) ? res.data : [] }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { success: false, error: "Unknown provider" }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async createPR(title, body, head = "main", base = "main") {
|
|
192
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
193
|
+
|
|
194
|
+
const { provider, owner, repo } = this.info
|
|
195
|
+
|
|
196
|
+
if (provider === "github") {
|
|
197
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`
|
|
198
|
+
const res = await apiRequest(url, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
token: this.token,
|
|
201
|
+
body: { title, body, head, base },
|
|
202
|
+
})
|
|
203
|
+
return { success: res.status === 201, data: res.data, url: res.data?.html_url }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (provider === "gitlab") {
|
|
207
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
208
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests`
|
|
209
|
+
const res = await apiRequest(url, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
token: this.token,
|
|
212
|
+
body: { title, description: body, source_branch: head, target_branch: base },
|
|
213
|
+
})
|
|
214
|
+
return { success: res.status === 201, data: res.data, url: res.data?.web_url }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { success: false, error: "Unknown provider" }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async reviewPR(prNumber, event, body) {
|
|
221
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
222
|
+
|
|
223
|
+
const { provider, owner, repo } = this.info
|
|
224
|
+
|
|
225
|
+
if (provider === "github") {
|
|
226
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`
|
|
227
|
+
const res = await apiRequest(url, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
token: this.token,
|
|
230
|
+
body: { event, body },
|
|
231
|
+
})
|
|
232
|
+
return { success: res.status === 200 || res.status === 201, data: res.data }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (provider === "gitlab") {
|
|
236
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
237
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests/${prNumber}/approvals`
|
|
238
|
+
const res = await apiRequest(url, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
token: this.token,
|
|
241
|
+
})
|
|
242
|
+
return { success: res.status === 201, data: res.data }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { success: false, error: "Unknown provider" }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async mergePR(prNumber) {
|
|
249
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
250
|
+
|
|
251
|
+
const { provider, owner, repo } = this.info
|
|
252
|
+
|
|
253
|
+
if (provider === "github") {
|
|
254
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/merge`
|
|
255
|
+
const res = await apiRequest(url, {
|
|
256
|
+
method: "PUT",
|
|
257
|
+
token: this.token,
|
|
258
|
+
body: { merge_method: "squash" },
|
|
259
|
+
})
|
|
260
|
+
return { success: res.status === 200, data: res.data }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (provider === "gitlab") {
|
|
264
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
265
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests/${prNumber}/merge`
|
|
266
|
+
const res = await apiRequest(url, {
|
|
267
|
+
method: "PUT",
|
|
268
|
+
token: this.token,
|
|
269
|
+
})
|
|
270
|
+
return { success: res.status === 200, data: res.data }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { success: false, error: "Unknown provider" }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ===== CODE REVIEW =====
|
|
277
|
+
async getPRFiles(prNumber) {
|
|
278
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
279
|
+
|
|
280
|
+
const { provider, owner, repo } = this.info
|
|
281
|
+
|
|
282
|
+
if (provider === "github") {
|
|
283
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/files`
|
|
284
|
+
const res = await apiRequest(url, { token: this.token })
|
|
285
|
+
return { success: true, files: Array.isArray(res.data) ? res.data : [] }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (provider === "gitlab") {
|
|
289
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
290
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/merge_requests/${prNumber}/changes`
|
|
291
|
+
const res = await apiRequest(url, { token: this.token })
|
|
292
|
+
return { success: true, files: res.data?.changes || [] }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { success: false, error: "Unknown provider" }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async addComment(prNumber, body, filePath, position) {
|
|
299
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
300
|
+
|
|
301
|
+
const { provider, owner, repo } = this.info
|
|
302
|
+
|
|
303
|
+
if (provider === "github" && filePath && position) {
|
|
304
|
+
// Get latest commit SHA
|
|
305
|
+
const prUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`
|
|
306
|
+
const prRes = await apiRequest(prUrl, { token: this.token })
|
|
307
|
+
const commitId = prRes.data?.head?.sha
|
|
308
|
+
|
|
309
|
+
if (commitId) {
|
|
310
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/comments`
|
|
311
|
+
const res = await apiRequest(url, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
token: this.token,
|
|
314
|
+
body: { body, path: filePath, position, commit_id: commitId },
|
|
315
|
+
})
|
|
316
|
+
return { success: res.status === 201, data: res.data }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Fallback: general comment
|
|
321
|
+
return await this.reviewPR(prNumber, "COMMENT", body)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ===== REPOS =====
|
|
325
|
+
async getRepoInfo() {
|
|
326
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
327
|
+
|
|
328
|
+
const { provider, owner, repo } = this.info
|
|
329
|
+
|
|
330
|
+
if (provider === "github") {
|
|
331
|
+
const url = `https://api.github.com/repos/${owner}/${repo}`
|
|
332
|
+
const res = await apiRequest(url, { token: this.token })
|
|
333
|
+
return { success: true, repo: res.data }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (provider === "gitlab") {
|
|
337
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
338
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}`
|
|
339
|
+
const res = await apiRequest(url, { token: this.token })
|
|
340
|
+
return { success: true, repo: res.data }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { success: false, error: "Unknown provider" }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async listBranches() {
|
|
347
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
348
|
+
|
|
349
|
+
const { provider, owner, repo } = this.info
|
|
350
|
+
|
|
351
|
+
if (provider === "github") {
|
|
352
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/branches`
|
|
353
|
+
const res = await apiRequest(url, { token: this.token })
|
|
354
|
+
return { success: true, branches: Array.isArray(res.data) ? res.data.map(b => b.name) : [] }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (provider === "gitlab") {
|
|
358
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
359
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/repository/branches`
|
|
360
|
+
const res = await apiRequest(url, { token: this.token })
|
|
361
|
+
return { success: true, branches: Array.isArray(res.data) ? res.data.map(b => b.name) : [] }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { success: false, error: "Unknown provider" }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async listReleases() {
|
|
368
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
369
|
+
|
|
370
|
+
const { provider, owner, repo } = this.info
|
|
371
|
+
|
|
372
|
+
if (provider === "github") {
|
|
373
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/releases`
|
|
374
|
+
const res = await apiRequest(url, { token: this.token })
|
|
375
|
+
return { success: true, releases: Array.isArray(res.data) ? res.data : [] }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (provider === "gitlab") {
|
|
379
|
+
const encoded = encodeURIComponent(`${owner}/${repo}`)
|
|
380
|
+
const url = `https://gitlab.com/api/v4/projects/${encoded}/releases`
|
|
381
|
+
const res = await apiRequest(url, { token: this.token })
|
|
382
|
+
return { success: true, releases: Array.isArray(res.data) ? res.data : [] }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { success: false, error: "Unknown provider" }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async createRelease(tag, name, body) {
|
|
389
|
+
if (!this.isConfigured()) return { success: false, error: "No git remote configured" }
|
|
390
|
+
|
|
391
|
+
const { provider, owner, repo } = this.info
|
|
392
|
+
|
|
393
|
+
if (provider === "github") {
|
|
394
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/releases`
|
|
395
|
+
const res = await apiRequest(url, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
token: this.token,
|
|
398
|
+
body: { tag_name: tag, name, body },
|
|
399
|
+
})
|
|
400
|
+
return { success: res.status === 201, data: res.data, url: res.data?.html_url }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { success: false, error: "Release creation only supported on GitHub" }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export default GitAPI
|