shiva-code 0.2.1 → 0.3.0

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,560 @@
1
+ import {
2
+ cache
3
+ } from "./chunk-G2G6UUWM.js";
4
+
5
+ // src/services/github.ts
6
+ import { execSync, spawnSync } from "child_process";
7
+ function isGhInstalled() {
8
+ try {
9
+ execSync("which gh", { stdio: "ignore" });
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ function isGhAuthenticated() {
16
+ try {
17
+ const result = spawnSync("gh", ["auth", "status"], {
18
+ encoding: "utf-8",
19
+ stdio: ["pipe", "pipe", "pipe"]
20
+ });
21
+ return result.status === 0;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function getGhUser() {
27
+ try {
28
+ const result = spawnSync("gh", ["api", "user", "--jq", ".login"], {
29
+ encoding: "utf-8",
30
+ stdio: ["pipe", "pipe", "pipe"]
31
+ });
32
+ if (result.status === 0 && result.stdout) {
33
+ return result.stdout.trim();
34
+ }
35
+ return null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ function runGh(args, cwd) {
41
+ try {
42
+ const result = spawnSync("gh", args, {
43
+ encoding: "utf-8",
44
+ stdio: ["pipe", "pipe", "pipe"],
45
+ cwd
46
+ });
47
+ if (result.status === 0 && result.stdout) {
48
+ return JSON.parse(result.stdout);
49
+ }
50
+ return null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+ function runGhRaw(args, cwd) {
56
+ try {
57
+ const result = spawnSync("gh", args, {
58
+ encoding: "utf-8",
59
+ stdio: ["pipe", "pipe", "pipe"],
60
+ cwd
61
+ });
62
+ if (result.status === 0 && result.stdout) {
63
+ return result.stdout.trim();
64
+ }
65
+ return null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ function isGitRepo(projectPath) {
71
+ try {
72
+ const result = spawnSync("git", ["rev-parse", "--git-dir"], {
73
+ encoding: "utf-8",
74
+ stdio: ["pipe", "pipe", "pipe"],
75
+ cwd: projectPath
76
+ });
77
+ return result.status === 0;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ function getCurrentBranch(projectPath) {
83
+ try {
84
+ const result = spawnSync("git", ["branch", "--show-current"], {
85
+ encoding: "utf-8",
86
+ stdio: ["pipe", "pipe", "pipe"],
87
+ cwd: projectPath
88
+ });
89
+ if (result.status === 0 && result.stdout) {
90
+ return result.stdout.trim() || "HEAD";
91
+ }
92
+ return "HEAD";
93
+ } catch {
94
+ return "HEAD";
95
+ }
96
+ }
97
+ function getRecentCommits(projectPath, count = 5) {
98
+ try {
99
+ const format = '{"sha": "%h", "message": "%s", "author": "%an", "date": "%ci"}';
100
+ const result = spawnSync(
101
+ "git",
102
+ ["log", `-${count}`, `--pretty=format:${format}`],
103
+ {
104
+ encoding: "utf-8",
105
+ stdio: ["pipe", "pipe", "pipe"],
106
+ cwd: projectPath
107
+ }
108
+ );
109
+ if (result.status === 0 && result.stdout) {
110
+ const lines = result.stdout.trim().split("\n").filter(Boolean);
111
+ return lines.map((line) => {
112
+ try {
113
+ return JSON.parse(line);
114
+ } catch {
115
+ return null;
116
+ }
117
+ }).filter((c) => c !== null);
118
+ }
119
+ return [];
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+ function getGitHubRemote(projectPath) {
125
+ try {
126
+ const result = spawnSync("git", ["remote", "get-url", "origin"], {
127
+ encoding: "utf-8",
128
+ stdio: ["pipe", "pipe", "pipe"],
129
+ cwd: projectPath
130
+ });
131
+ if (result.status === 0 && result.stdout) {
132
+ const url = result.stdout.trim();
133
+ if (url.includes("github.com")) {
134
+ return url;
135
+ }
136
+ }
137
+ const remotesResult = spawnSync("git", ["remote", "-v"], {
138
+ encoding: "utf-8",
139
+ stdio: ["pipe", "pipe", "pipe"],
140
+ cwd: projectPath
141
+ });
142
+ if (remotesResult.status === 0 && remotesResult.stdout) {
143
+ const lines = remotesResult.stdout.split("\n");
144
+ for (const line of lines) {
145
+ if (line.includes("github.com") && line.includes("(fetch)")) {
146
+ const match = line.match(/\s+(.+)\s+\(fetch\)/);
147
+ if (match) return match[1];
148
+ }
149
+ }
150
+ }
151
+ return null;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+ function parseGitHubUrl(url) {
157
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^.]+)(?:\.git)?/);
158
+ if (sshMatch) {
159
+ return { owner: sshMatch[1], name: sshMatch[2] };
160
+ }
161
+ const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?/);
162
+ if (httpsMatch) {
163
+ return { owner: httpsMatch[1], name: httpsMatch[2] };
164
+ }
165
+ return null;
166
+ }
167
+ function getRepoInfo(projectPath) {
168
+ if (!isGitRepo(projectPath)) {
169
+ return null;
170
+ }
171
+ const remoteUrl = getGitHubRemote(projectPath);
172
+ if (!remoteUrl) {
173
+ return null;
174
+ }
175
+ const parsed = parseGitHubUrl(remoteUrl);
176
+ if (!parsed) {
177
+ return null;
178
+ }
179
+ const repoData = runGh(
180
+ ["repo", "view", `${parsed.owner}/${parsed.name}`, "--json", "defaultBranchRef,isPrivate,url"],
181
+ projectPath
182
+ );
183
+ return {
184
+ owner: parsed.owner,
185
+ name: parsed.name,
186
+ fullName: `${parsed.owner}/${parsed.name}`,
187
+ url: repoData?.url || `https://github.com/${parsed.owner}/${parsed.name}`,
188
+ defaultBranch: repoData?.defaultBranchRef?.name || "main",
189
+ isPrivate: repoData?.isPrivate || false
190
+ };
191
+ }
192
+ function isGitHubRepo(projectPath) {
193
+ return getRepoInfo(projectPath) !== null;
194
+ }
195
+ function getOpenIssues(repo, options) {
196
+ const cacheKey = `${repo}:${options?.assignee || ""}:${options?.labels?.join(",") || ""}`;
197
+ if (!options?.skipCache && !options?.assignee && !options?.labels) {
198
+ const cached = cache.getGitHubIssues(repo);
199
+ if (cached) {
200
+ return options?.limit ? cached.slice(0, options.limit) : cached;
201
+ }
202
+ }
203
+ const args = [
204
+ "issue",
205
+ "list",
206
+ "-R",
207
+ repo,
208
+ "--json",
209
+ "number,title,state,author,assignees,labels,body,url,createdAt,updatedAt"
210
+ ];
211
+ if (options?.assignee) {
212
+ args.push("--assignee", options.assignee);
213
+ }
214
+ if (options?.labels && options.labels.length > 0) {
215
+ args.push("--label", options.labels.join(","));
216
+ }
217
+ if (options?.limit) {
218
+ args.push("--limit", String(options.limit));
219
+ }
220
+ const issues = runGh(args);
221
+ if (!issues) {
222
+ return [];
223
+ }
224
+ const mappedIssues = issues.map((issue) => ({
225
+ number: issue.number,
226
+ title: issue.title,
227
+ state: issue.state.toLowerCase(),
228
+ author: issue.author?.login || "unknown",
229
+ assignees: issue.assignees?.map((a) => a.login) || [],
230
+ labels: issue.labels?.map((l) => l.name) || [],
231
+ body: issue.body || "",
232
+ url: issue.url,
233
+ createdAt: issue.createdAt,
234
+ updatedAt: issue.updatedAt,
235
+ repo
236
+ }));
237
+ if (!options?.assignee && !options?.labels) {
238
+ cache.setGitHubIssues(repo, mappedIssues);
239
+ }
240
+ return mappedIssues;
241
+ }
242
+ function getIssue(repo, number) {
243
+ const issue = runGh(
244
+ ["issue", "view", String(number), "-R", repo, "--json", "number,title,state,author,assignees,labels,body,url,createdAt,updatedAt"]
245
+ );
246
+ if (!issue) {
247
+ return null;
248
+ }
249
+ return {
250
+ number: issue.number,
251
+ title: issue.title,
252
+ state: issue.state.toLowerCase(),
253
+ author: issue.author?.login || "unknown",
254
+ assignees: issue.assignees?.map((a) => a.login) || [],
255
+ labels: issue.labels?.map((l) => l.name) || [],
256
+ body: issue.body || "",
257
+ url: issue.url,
258
+ createdAt: issue.createdAt,
259
+ updatedAt: issue.updatedAt,
260
+ repo
261
+ };
262
+ }
263
+ function findMentionedIssueNumbers(text) {
264
+ const matches = text.match(/#(\d+)/g);
265
+ if (!matches) {
266
+ return [];
267
+ }
268
+ return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)))];
269
+ }
270
+ function getOpenPRs(repo, options) {
271
+ if (!options?.skipCache && !options?.author && (!options?.state || options.state === "open")) {
272
+ const cached = cache.getGitHubPRs(repo);
273
+ if (cached) {
274
+ return options?.limit ? cached.slice(0, options.limit) : cached;
275
+ }
276
+ }
277
+ const args = [
278
+ "pr",
279
+ "list",
280
+ "-R",
281
+ repo,
282
+ "--json",
283
+ "number,title,state,author,headRefName,baseRefName,isDraft,reviewDecision,statusCheckRollup,url,body,createdAt,updatedAt"
284
+ ];
285
+ if (options?.author) {
286
+ args.push("--author", options.author);
287
+ }
288
+ if (options?.state && options.state !== "all") {
289
+ args.push("--state", options.state);
290
+ }
291
+ if (options?.limit) {
292
+ args.push("--limit", String(options.limit));
293
+ }
294
+ const prs = runGh(args);
295
+ if (!prs) {
296
+ return [];
297
+ }
298
+ const mappedPRs = prs.map((pr) => ({
299
+ number: pr.number,
300
+ title: pr.title,
301
+ state: pr.state.toLowerCase(),
302
+ author: pr.author?.login || "unknown",
303
+ headBranch: pr.headRefName,
304
+ baseBranch: pr.baseRefName,
305
+ isDraft: pr.isDraft,
306
+ reviewDecision: mapReviewDecision(pr.reviewDecision),
307
+ checksStatus: mapChecksStatus(pr.statusCheckRollup),
308
+ url: pr.url,
309
+ body: pr.body || "",
310
+ repo,
311
+ createdAt: pr.createdAt,
312
+ updatedAt: pr.updatedAt
313
+ }));
314
+ if (!options?.author && (!options?.state || options.state === "open")) {
315
+ cache.setGitHubPRs(repo, mappedPRs);
316
+ }
317
+ return mappedPRs;
318
+ }
319
+ function getPRForBranch(repo, branch) {
320
+ const prs = runGh(
321
+ ["pr", "list", "-R", repo, "--head", branch, "--json", "number,title,state,author,headRefName,baseRefName,isDraft,reviewDecision,statusCheckRollup,url,body,createdAt,updatedAt"]
322
+ );
323
+ if (!prs || prs.length === 0) {
324
+ return null;
325
+ }
326
+ const pr = prs[0];
327
+ return {
328
+ number: pr.number,
329
+ title: pr.title,
330
+ state: pr.state.toLowerCase(),
331
+ author: pr.author?.login || "unknown",
332
+ headBranch: pr.headRefName,
333
+ baseBranch: pr.baseRefName,
334
+ isDraft: pr.isDraft,
335
+ reviewDecision: mapReviewDecision(pr.reviewDecision),
336
+ checksStatus: mapChecksStatus(pr.statusCheckRollup),
337
+ url: pr.url,
338
+ body: pr.body || "",
339
+ repo,
340
+ createdAt: pr.createdAt,
341
+ updatedAt: pr.updatedAt
342
+ };
343
+ }
344
+ function getPR(repo, number) {
345
+ const pr = runGh(
346
+ ["pr", "view", String(number), "-R", repo, "--json", "number,title,state,author,headRefName,baseRefName,isDraft,reviewDecision,statusCheckRollup,url,body,createdAt,updatedAt"]
347
+ );
348
+ if (!pr) {
349
+ return null;
350
+ }
351
+ return {
352
+ number: pr.number,
353
+ title: pr.title,
354
+ state: pr.state.toLowerCase(),
355
+ author: pr.author?.login || "unknown",
356
+ headBranch: pr.headRefName,
357
+ baseBranch: pr.baseRefName,
358
+ isDraft: pr.isDraft,
359
+ reviewDecision: mapReviewDecision(pr.reviewDecision),
360
+ checksStatus: mapChecksStatus(pr.statusCheckRollup),
361
+ url: pr.url,
362
+ body: pr.body || "",
363
+ repo,
364
+ createdAt: pr.createdAt,
365
+ updatedAt: pr.updatedAt
366
+ };
367
+ }
368
+ function mapReviewDecision(decision) {
369
+ if (!decision) return null;
370
+ const lower = decision.toLowerCase();
371
+ if (lower === "approved") return "approved";
372
+ if (lower === "changes_requested") return "changes_requested";
373
+ if (lower === "review_required") return "review_required";
374
+ return null;
375
+ }
376
+ function mapChecksStatus(checks) {
377
+ if (!checks || checks.length === 0) return null;
378
+ const hasFailure = checks.some((c) => c.conclusion === "FAILURE");
379
+ const hasPending = checks.some((c) => c.status !== "COMPLETED");
380
+ if (hasFailure) return "failure";
381
+ if (hasPending) return "pending";
382
+ return "success";
383
+ }
384
+ function getCIStatus(repo, branch) {
385
+ const runs = runGh(
386
+ ["run", "list", "-R", repo, "--branch", branch, "--limit", "1", "--json", "status,conclusion"]
387
+ );
388
+ if (!runs || runs.length === 0) {
389
+ return "none";
390
+ }
391
+ const run = runs[0];
392
+ if (run.status !== "completed") {
393
+ return "pending";
394
+ }
395
+ if (run.conclusion === "success") {
396
+ return "success";
397
+ }
398
+ return "failure";
399
+ }
400
+ async function generateGitHubContext(projectPath, skipCache = false) {
401
+ if (!skipCache) {
402
+ const cached = cache.getGitHubContext(projectPath);
403
+ if (cached) {
404
+ return cached;
405
+ }
406
+ }
407
+ if (!isGhInstalled() || !isGhAuthenticated()) {
408
+ return null;
409
+ }
410
+ const repo = getRepoInfo(projectPath);
411
+ if (!repo) {
412
+ return null;
413
+ }
414
+ const currentBranch = getCurrentBranch(projectPath);
415
+ const currentUser = getGhUser();
416
+ const currentPR = getPRForBranch(repo.fullName, currentBranch);
417
+ const openPRs = getOpenPRs(repo.fullName, { limit: 10, skipCache });
418
+ const assignedIssues = currentUser ? getOpenIssues(repo.fullName, { assignee: currentUser, limit: 10, skipCache }) : [];
419
+ const recentCommits = getRecentCommits(projectPath, 5);
420
+ const ciStatus = getCIStatus(repo.fullName, currentBranch);
421
+ const mentionedNumbers = /* @__PURE__ */ new Set();
422
+ findMentionedIssueNumbers(currentBranch).forEach((n) => mentionedNumbers.add(n));
423
+ recentCommits.forEach((c) => {
424
+ findMentionedIssueNumbers(c.message).forEach((n) => mentionedNumbers.add(n));
425
+ });
426
+ const assignedNumbers = new Set(assignedIssues.map((i) => i.number));
427
+ const mentionedIssues = [];
428
+ for (const num of mentionedNumbers) {
429
+ if (!assignedNumbers.has(num)) {
430
+ const issue = getIssue(repo.fullName, num);
431
+ if (issue && issue.state === "open") {
432
+ mentionedIssues.push(issue);
433
+ }
434
+ }
435
+ }
436
+ const context = {
437
+ repo,
438
+ currentBranch,
439
+ currentPR,
440
+ openPRs,
441
+ assignedIssues,
442
+ mentionedIssues,
443
+ recentCommits,
444
+ ciStatus,
445
+ lastFetched: (/* @__PURE__ */ new Date()).toISOString()
446
+ };
447
+ cache.setGitHubContext(projectPath, context);
448
+ return context;
449
+ }
450
+ function invalidateGitHubCache(repo) {
451
+ if (repo) {
452
+ cache.invalidateGitHubRepo(repo);
453
+ } else {
454
+ cache.invalidateGitHub();
455
+ }
456
+ }
457
+ function formatContextAsMarkdown(context) {
458
+ const lines = [];
459
+ lines.push("# GitHub Context");
460
+ lines.push("");
461
+ if (context.repo) {
462
+ const visibility = context.repo.isPrivate ? "private" : "public";
463
+ lines.push(`## Repository`);
464
+ lines.push(`**${context.repo.fullName}** (${visibility})`);
465
+ lines.push(`Branch: \`${context.currentBranch}\` \u2192 \`${context.repo.defaultBranch}\``);
466
+ lines.push("");
467
+ }
468
+ if (context.currentPR) {
469
+ const pr = context.currentPR;
470
+ const statusIcons = {
471
+ success: "\u2713",
472
+ failure: "\u2717",
473
+ pending: "\u23F3",
474
+ null: "\u25CB"
475
+ };
476
+ const reviewIcons = {
477
+ approved: "\u2713 Approved",
478
+ changes_requested: "\u274C Changes requested",
479
+ review_required: "\u{1F440} Review required",
480
+ null: "No review"
481
+ };
482
+ lines.push("## Your Current PR");
483
+ lines.push(`### #${pr.number}: ${pr.title}`);
484
+ lines.push(`- Status: ${pr.state}`);
485
+ lines.push(`- CI: ${statusIcons[pr.checksStatus || "null"]} ${pr.checksStatus || "none"}`);
486
+ lines.push(`- Review: ${reviewIcons[pr.reviewDecision || "null"]}`);
487
+ lines.push(`- Branch: \`${pr.headBranch}\` \u2192 \`${pr.baseBranch}\``);
488
+ lines.push("");
489
+ }
490
+ if (context.assignedIssues.length > 0) {
491
+ lines.push("## Assigned to You");
492
+ for (const issue of context.assignedIssues) {
493
+ const labels = issue.labels.length > 0 ? ` [${issue.labels.join(", ")}]` : "";
494
+ lines.push(`- \u{1F534} #${issue.number}: ${issue.title}${labels}`);
495
+ }
496
+ lines.push("");
497
+ }
498
+ if (context.mentionedIssues.length > 0) {
499
+ lines.push("## Related Issues (mentioned in branch/commits)");
500
+ for (const issue of context.mentionedIssues) {
501
+ lines.push(`- #${issue.number}: ${issue.title}`);
502
+ }
503
+ lines.push("");
504
+ }
505
+ const otherPRs = context.openPRs.filter((pr) => pr.number !== context.currentPR?.number);
506
+ if (otherPRs.length > 0) {
507
+ lines.push("## Other Open PRs");
508
+ for (const pr of otherPRs.slice(0, 5)) {
509
+ const ciIcon = pr.checksStatus === "success" ? "\u2713" : pr.checksStatus === "failure" ? "\u2717" : "\u25CB";
510
+ lines.push(`- #${pr.number}: ${pr.title} (${ciIcon} @${pr.author})`);
511
+ }
512
+ lines.push("");
513
+ }
514
+ if (context.recentCommits.length > 0) {
515
+ lines.push("## Recent Commits");
516
+ for (const commit of context.recentCommits) {
517
+ lines.push(`- \`${commit.sha}\` ${commit.message}`);
518
+ }
519
+ lines.push("");
520
+ }
521
+ if (context.ciStatus !== "none") {
522
+ const statusText = {
523
+ success: "\u2713 All checks passed",
524
+ failure: "\u2717 Checks failing",
525
+ pending: "\u23F3 Checks running",
526
+ none: "No checks"
527
+ };
528
+ lines.push("## CI/CD");
529
+ lines.push(`Status: ${statusText[context.ciStatus]}`);
530
+ lines.push("");
531
+ }
532
+ lines.push("---");
533
+ lines.push("*Context generated by SHIVA Code*");
534
+ return lines.join("\n");
535
+ }
536
+
537
+ export {
538
+ isGhInstalled,
539
+ isGhAuthenticated,
540
+ getGhUser,
541
+ runGh,
542
+ runGhRaw,
543
+ isGitRepo,
544
+ getCurrentBranch,
545
+ getRecentCommits,
546
+ getGitHubRemote,
547
+ parseGitHubUrl,
548
+ getRepoInfo,
549
+ isGitHubRepo,
550
+ getOpenIssues,
551
+ getIssue,
552
+ findMentionedIssueNumbers,
553
+ getOpenPRs,
554
+ getPRForBranch,
555
+ getPR,
556
+ getCIStatus,
557
+ generateGitHubContext,
558
+ invalidateGitHubCache,
559
+ formatContextAsMarkdown
560
+ };