@sabahattinkalkan/gpulse 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,702 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command7 } from "commander";
5
+
6
+ // src/commands/standup.ts
7
+ import { Command } from "commander";
8
+ import chalk2 from "chalk";
9
+ import ora from "ora";
10
+ import dayjs from "dayjs";
11
+
12
+ // src/lib/github.ts
13
+ import { Octokit } from "octokit";
14
+ var octokit;
15
+ function getClient() {
16
+ if (!octokit) {
17
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
18
+ if (!token) {
19
+ throw new TokenError();
20
+ }
21
+ octokit = new Octokit({ auth: token });
22
+ }
23
+ return octokit;
24
+ }
25
+ var TokenError = class extends Error {
26
+ constructor() {
27
+ super("GitHub token not found. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
28
+ this.name = "TokenError";
29
+ }
30
+ };
31
+ async function getAuthenticatedUser() {
32
+ const client = getClient();
33
+ const { data } = await client.rest.users.getAuthenticated();
34
+ return data;
35
+ }
36
+ async function getRepoFromCwd() {
37
+ try {
38
+ const { execSync } = await import("child_process");
39
+ const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
40
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
41
+ if (match) return { owner: match[1], repo: match[2] };
42
+ } catch {
43
+ }
44
+ return null;
45
+ }
46
+ function parseRepoArg(repoArg) {
47
+ if (!repoArg) return null;
48
+ const parts = repoArg.split("/");
49
+ if (parts.length === 2) return { owner: parts[0], repo: parts[1] };
50
+ return null;
51
+ }
52
+ async function resolveRepo(repoArg) {
53
+ const parsed = parseRepoArg(repoArg);
54
+ if (parsed) return parsed;
55
+ const fromCwd = await getRepoFromCwd();
56
+ if (fromCwd) return fromCwd;
57
+ throw new Error("Could not determine repository. Use --repo owner/repo or run inside a git repo.");
58
+ }
59
+
60
+ // src/utils/helpers.ts
61
+ import chalk from "chalk";
62
+ function handleError(error) {
63
+ if (error instanceof TokenError) {
64
+ console.error("");
65
+ console.error(chalk.red(" \u2717 GitHub token not found"));
66
+ console.error("");
67
+ console.error(chalk.gray(" Set one of these environment variables:"));
68
+ console.error(chalk.white(" export GITHUB_TOKEN=ghp_your_token_here"));
69
+ console.error(chalk.white(" export GH_TOKEN=ghp_your_token_here"));
70
+ console.error("");
71
+ console.error(chalk.gray(" Create a token at: https://github.com/settings/tokens"));
72
+ console.error("");
73
+ process.exit(1);
74
+ }
75
+ if (error instanceof Error) {
76
+ const msg = error.message;
77
+ if (msg.includes("Not Found") || error.status === 404) {
78
+ console.error(chalk.red("\n \u2717 Repository not found. Check the owner/repo name.\n"));
79
+ } else if (error.status === 403) {
80
+ console.error(chalk.red("\n \u2717 Rate limit exceeded or insufficient permissions.\n"));
81
+ } else {
82
+ console.error(chalk.red(`
83
+ \u2717 ${msg}
84
+ `));
85
+ }
86
+ } else {
87
+ console.error(chalk.red("\n \u2717 An unexpected error occurred.\n"));
88
+ }
89
+ process.exit(1);
90
+ }
91
+ function truncate(str, max) {
92
+ return str.length > max ? str.slice(0, max - 1) + "\u2026" : str;
93
+ }
94
+ function pluralize(count, singular, plural) {
95
+ return count === 1 ? `${count} ${singular}` : `${count} ${plural || singular + "s"}`;
96
+ }
97
+ function bar(value, max, width = 20) {
98
+ const filled = Math.round(value / max * width);
99
+ return chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(width - filled));
100
+ }
101
+
102
+ // src/commands/standup.ts
103
+ var standupCommand = new Command("standup").description("What did you do since yesterday? Commits, PRs, reviews, issues").option("-u, --user <username>", "GitHub username (default: authenticated user)").option("-d, --days <number>", "Look back N days", "1").option("-r, --repo <owner/repo>", "Specific repo (default: all repos)").action(async (options) => {
104
+ const spinner = ora("Fetching your activity...").start();
105
+ try {
106
+ const client = getClient();
107
+ const days = parseInt(options.days);
108
+ const since = dayjs().subtract(days, "day").startOf("day").toISOString();
109
+ let username = options.user;
110
+ if (!username) {
111
+ const user = await getAuthenticatedUser();
112
+ username = user.login;
113
+ }
114
+ const { data: events } = await client.rest.activity.listEventsForAuthenticatedUser({
115
+ username,
116
+ per_page: 100
117
+ });
118
+ let recentEvents = events.filter(
119
+ (e) => dayjs(e.created_at).isAfter(since)
120
+ );
121
+ if (options.repo) {
122
+ recentEvents = recentEvents.filter((e) => e.repo.name === options.repo);
123
+ }
124
+ const pushEvents = recentEvents.filter((e) => e.type === "PushEvent");
125
+ const prEvents = recentEvents.filter((e) => e.type === "PullRequestEvent");
126
+ const reviewEvents = recentEvents.filter((e) => e.type === "PullRequestReviewEvent");
127
+ const issueEvents = recentEvents.filter((e) => e.type === "IssuesEvent");
128
+ spinner.stop();
129
+ console.log("");
130
+ console.log(chalk2.bold(` Standup for @${username}`));
131
+ console.log(chalk2.gray(` Since ${dayjs(since).format("MMM D, YYYY h:mm A")}`));
132
+ console.log("");
133
+ if (pushEvents.length > 0) {
134
+ console.log(chalk2.cyan.bold(" Commits"));
135
+ const commits = [];
136
+ for (const event of pushEvents) {
137
+ const payload = event.payload;
138
+ const repo = event.repo.name;
139
+ for (const commit of payload.commits || []) {
140
+ commits.push({ repo, message: commit.message.split("\n")[0], sha: commit.sha.slice(0, 7) });
141
+ }
142
+ }
143
+ for (const c of commits.slice(0, 20)) {
144
+ console.log(chalk2.gray(` ${c.sha}`) + ` ${c.message}` + chalk2.gray(` (${c.repo})`));
145
+ }
146
+ if (commits.length > 20) {
147
+ console.log(chalk2.gray(` ... and ${commits.length - 20} more`));
148
+ }
149
+ console.log("");
150
+ }
151
+ if (prEvents.length > 0) {
152
+ console.log(chalk2.green.bold(" Pull Requests"));
153
+ for (const event of prEvents) {
154
+ const payload = event.payload;
155
+ const action = payload.action;
156
+ const title = payload.pull_request?.title;
157
+ const number = payload.pull_request?.number;
158
+ const icon = action === "opened" ? "+" : action === "closed" ? "x" : "~";
159
+ console.log(` ${icon} #${number} ${title}` + chalk2.gray(` (${event.repo.name})`));
160
+ }
161
+ console.log("");
162
+ }
163
+ if (reviewEvents.length > 0) {
164
+ console.log(chalk2.magenta.bold(" Reviews"));
165
+ for (const event of reviewEvents) {
166
+ const payload = event.payload;
167
+ const title = payload.pull_request?.title;
168
+ const number = payload.pull_request?.number;
169
+ console.log(` > #${number} ${title}` + chalk2.gray(` (${event.repo.name})`));
170
+ }
171
+ console.log("");
172
+ }
173
+ if (issueEvents.length > 0) {
174
+ console.log(chalk2.yellow.bold(" Issues"));
175
+ for (const event of issueEvents) {
176
+ const payload = event.payload;
177
+ const action = payload.action;
178
+ const title = payload.issue?.title;
179
+ const number = payload.issue?.number;
180
+ const icon = action === "opened" ? "+" : "x";
181
+ console.log(` ${icon} #${number} ${title}` + chalk2.gray(` (${event.repo.name})`));
182
+ }
183
+ console.log("");
184
+ }
185
+ if (recentEvents.length === 0) {
186
+ console.log(chalk2.gray(" No activity found. Enjoy your day off!"));
187
+ }
188
+ const totalCommits = pushEvents.reduce((sum, e) => sum + (e.payload.commits?.length || 0), 0);
189
+ console.log(chalk2.gray(" " + "-".repeat(40)));
190
+ console.log(chalk2.bold(" Summary: ") + `${totalCommits} commits, ${prEvents.length} PRs, ${reviewEvents.length} reviews, ${issueEvents.length} issues`);
191
+ console.log("");
192
+ } catch (error) {
193
+ spinner.fail("Failed to fetch activity");
194
+ handleError(error);
195
+ }
196
+ });
197
+
198
+ // src/commands/stats.ts
199
+ import { Command as Command2 } from "commander";
200
+ import chalk4 from "chalk";
201
+ import ora2 from "ora";
202
+ import dayjs2 from "dayjs";
203
+
204
+ // src/lib/format.ts
205
+ import chalk3 from "chalk";
206
+ function separator() {
207
+ return chalk3.gray(" " + "\u2500".repeat(40));
208
+ }
209
+ function scoreColor(score) {
210
+ if (score >= 90) return chalk3.green;
211
+ if (score >= 70) return chalk3.yellow;
212
+ if (score >= 50) return chalk3.hex("#FFA500");
213
+ return chalk3.red;
214
+ }
215
+ function scoreGrade(score) {
216
+ if (score >= 90) return "A";
217
+ if (score >= 80) return "B";
218
+ if (score >= 70) return "C";
219
+ if (score >= 60) return "D";
220
+ return "F";
221
+ }
222
+ function check(ok, label) {
223
+ return ok ? chalk3.green(` \u2713 ${label}`) : chalk3.red(` \u2717 ${label}`);
224
+ }
225
+ function timeAgo(dateStr) {
226
+ const now = Date.now();
227
+ const then = new Date(dateStr).getTime();
228
+ const diff = now - then;
229
+ const minutes = Math.floor(diff / 6e4);
230
+ if (minutes < 60) return `${minutes}m ago`;
231
+ const hours = Math.floor(minutes / 60);
232
+ if (hours < 24) return `${hours}h ago`;
233
+ const days = Math.floor(hours / 24);
234
+ if (days < 30) return `${days}d ago`;
235
+ const months = Math.floor(days / 30);
236
+ return `${months}mo ago`;
237
+ }
238
+
239
+ // src/commands/stats.ts
240
+ var statsCommand = new Command2("stats").description("Repository statistics \u2014 commits, contributors, languages, top authors").argument("[repo]", "owner/repo (default: current directory)").action(async (repoArg) => {
241
+ const spinner = ora2("Fetching repository stats...").start();
242
+ try {
243
+ const client = getClient();
244
+ const { owner, repo } = await resolveRepo(repoArg);
245
+ const [repoData, contributorsData, languagesData, commitsPage] = await Promise.all([
246
+ client.rest.repos.get({ owner, repo }),
247
+ client.rest.repos.listContributors({ owner, repo, per_page: 10 }).catch(() => ({ data: [] })),
248
+ client.rest.repos.listLanguages({ owner, repo }),
249
+ client.rest.repos.listCommits({ owner, repo, per_page: 5 })
250
+ ]);
251
+ const r = repoData.data;
252
+ const contributors = contributorsData.data;
253
+ const languages = languagesData.data;
254
+ const recentCommits = commitsPage.data;
255
+ let weeklyAvg = 0;
256
+ try {
257
+ const { data: participation } = await client.rest.repos.getParticipationStats({ owner, repo });
258
+ const allWeeks = participation.all || [];
259
+ const nonZero = allWeeks.filter((w) => w > 0);
260
+ weeklyAvg = nonZero.length > 0 ? Math.round(nonZero.reduce((a, b) => a + b, 0) / nonZero.length) : 0;
261
+ } catch {
262
+ }
263
+ spinner.stop();
264
+ const age = dayjs2().diff(dayjs2(r.created_at), "month");
265
+ const ageStr = age >= 12 ? `${Math.floor(age / 12)}y ${age % 12}m` : `${age}m`;
266
+ console.log("");
267
+ console.log(chalk4.bold(` ${owner}/${repo}`));
268
+ if (r.description) console.log(chalk4.gray(` ${r.description}`));
269
+ console.log("");
270
+ console.log(chalk4.cyan.bold(" Overview"));
271
+ console.log(` Stars: ${chalk4.yellow(String(r.stargazers_count))} Forks: ${chalk4.blue(String(r.forks_count))} Watchers: ${r.subscribers_count}`);
272
+ console.log(` Age: ${ageStr} Size: ${(r.size / 1024).toFixed(1)} MB Default branch: ${r.default_branch}`);
273
+ console.log(` Open issues: ${r.open_issues_count} License: ${r.license?.spdx_id || "None"}`);
274
+ if (weeklyAvg > 0) console.log(` Avg weekly commits: ${weeklyAvg}`);
275
+ console.log("");
276
+ if (Object.keys(languages).length > 0) {
277
+ console.log(chalk4.cyan.bold(" Languages"));
278
+ const total = Object.values(languages).reduce((a, b) => a + b, 0);
279
+ const sorted = Object.entries(languages).sort((a, b) => b[1] - a[1]);
280
+ for (const [lang, bytes] of sorted.slice(0, 8)) {
281
+ const pct = (bytes / total * 100).toFixed(1);
282
+ console.log(` ${bar(bytes, total, 15)} ${pct}% ${lang}`);
283
+ }
284
+ console.log("");
285
+ }
286
+ if (Array.isArray(contributors) && contributors.length > 0) {
287
+ console.log(chalk4.cyan.bold(" Top Contributors"));
288
+ const maxContribs = contributors[0]?.contributions || 1;
289
+ for (const c of contributors.slice(0, 5)) {
290
+ console.log(` ${bar(c.contributions, maxContribs, 10)} ${c.contributions} ${c.login}`);
291
+ }
292
+ console.log("");
293
+ }
294
+ if (recentCommits.length > 0) {
295
+ console.log(chalk4.cyan.bold(" Recent Commits"));
296
+ for (const c of recentCommits) {
297
+ const sha = c.sha.slice(0, 7);
298
+ const msg = c.commit.message.split("\n")[0].slice(0, 60);
299
+ const author = c.author?.login || c.commit.author?.name || "unknown";
300
+ const date = dayjs2(c.commit.author?.date).format("MMM D");
301
+ console.log(chalk4.gray(` ${sha}`) + ` ${msg}` + chalk4.gray(` \u2014 ${author}, ${date}`));
302
+ }
303
+ console.log("");
304
+ }
305
+ console.log(separator());
306
+ console.log(chalk4.bold(` ${pluralize(r.stargazers_count, "star")} | ${pluralize(r.forks_count, "fork")} | ${pluralize(Array.isArray(contributors) ? contributors.length : 0, "contributor")}`));
307
+ console.log("");
308
+ } catch (error) {
309
+ spinner.fail("Failed to fetch stats");
310
+ handleError(error);
311
+ }
312
+ });
313
+
314
+ // src/commands/changelog.ts
315
+ import { Command as Command3 } from "commander";
316
+ import chalk5 from "chalk";
317
+ import ora3 from "ora";
318
+ import { writeFileSync } from "fs";
319
+ import dayjs3 from "dayjs";
320
+ function categorize(message) {
321
+ const sha = "";
322
+ const author = "";
323
+ const conventionalMatch = message.match(/^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)/);
324
+ if (conventionalMatch) {
325
+ return {
326
+ type: conventionalMatch[1],
327
+ scope: conventionalMatch[2],
328
+ message: conventionalMatch[4],
329
+ sha,
330
+ author
331
+ };
332
+ }
333
+ const lower = message.toLowerCase();
334
+ if (lower.startsWith("fix") || lower.includes("bugfix")) return { type: "fix", message, sha, author };
335
+ if (lower.startsWith("add") || lower.includes("feature")) return { type: "feat", message, sha, author };
336
+ if (lower.startsWith("doc") || lower.includes("readme")) return { type: "docs", message, sha, author };
337
+ return { type: "other", message, sha, author };
338
+ }
339
+ var TYPE_LABELS = {
340
+ feat: "Features",
341
+ fix: "Bug Fixes",
342
+ docs: "Documentation",
343
+ refactor: "Refactoring",
344
+ perf: "Performance",
345
+ test: "Tests",
346
+ ci: "CI/CD",
347
+ chore: "Chores",
348
+ style: "Style",
349
+ build: "Build",
350
+ other: "Other Changes"
351
+ };
352
+ var changelogCommand = new Command3("changelog").description("Generate changelog from commits between tags").argument("[repo]", "owner/repo (default: current directory)").option("-f, --from <tag>", "From tag (default: previous tag)").option("-t, --to <tag>", "To tag (default: HEAD)").option("-o, --output <file>", "Write to file (e.g. CHANGELOG.md)").action(async (repoArg, options) => {
353
+ const spinner = ora3("Generating changelog...").start();
354
+ try {
355
+ const client = getClient();
356
+ const { owner, repo } = await resolveRepo(repoArg);
357
+ const { data: tags } = await client.rest.repos.listTags({ owner, repo, per_page: 10 });
358
+ if (tags.length === 0) {
359
+ spinner.stop();
360
+ console.log(chalk5.yellow("\n No tags found. Showing last 30 commits instead.\n"));
361
+ }
362
+ let base;
363
+ let head;
364
+ if (options.from) {
365
+ base = options.from;
366
+ } else if (tags.length >= 2) {
367
+ base = tags[1].name;
368
+ }
369
+ if (options.to) {
370
+ head = options.to;
371
+ } else if (tags.length >= 1) {
372
+ head = tags[0].name;
373
+ }
374
+ let commits;
375
+ if (base && head) {
376
+ const { data: comparison } = await client.rest.repos.compareCommits({
377
+ owner,
378
+ repo,
379
+ base,
380
+ head
381
+ });
382
+ commits = comparison.commits;
383
+ } else {
384
+ const { data } = await client.rest.repos.listCommits({ owner, repo, per_page: 30 });
385
+ commits = data;
386
+ head = head || "HEAD";
387
+ base = base || "initial";
388
+ }
389
+ spinner.stop();
390
+ const entries = commits.map((c) => {
391
+ const msg = c.commit.message.split("\n")[0];
392
+ const entry = categorize(msg);
393
+ entry.sha = c.sha.slice(0, 7);
394
+ entry.author = c.author?.login || c.commit.author?.name || "unknown";
395
+ return entry;
396
+ });
397
+ const groups = {};
398
+ for (const entry of entries) {
399
+ if (!groups[entry.type]) groups[entry.type] = [];
400
+ groups[entry.type].push(entry);
401
+ }
402
+ const lines = [];
403
+ const title = `## ${head || "Unreleased"}${base ? ` (since ${base})` : ""} \u2014 ${dayjs3().format("YYYY-MM-DD")}`;
404
+ lines.push(title);
405
+ lines.push("");
406
+ const typeOrder = ["feat", "fix", "perf", "refactor", "docs", "test", "ci", "build", "chore", "style", "other"];
407
+ for (const type of typeOrder) {
408
+ const items = groups[type];
409
+ if (!items) continue;
410
+ lines.push(`### ${TYPE_LABELS[type] || type}`);
411
+ for (const item of items) {
412
+ const scope = item.scope ? `**${item.scope}:** ` : "";
413
+ lines.push(`- ${scope}${item.message} (\`${item.sha}\` by @${item.author})`);
414
+ }
415
+ lines.push("");
416
+ }
417
+ const output = lines.join("\n");
418
+ console.log("");
419
+ for (const line of lines) {
420
+ if (line.startsWith("## ")) {
421
+ console.log(chalk5.bold(` ${line}`));
422
+ } else if (line.startsWith("### ")) {
423
+ console.log(chalk5.cyan(` ${line}`));
424
+ } else if (line.startsWith("- ")) {
425
+ console.log(` ${line}`);
426
+ } else {
427
+ console.log(line);
428
+ }
429
+ }
430
+ if (options.output) {
431
+ writeFileSync(options.output, output);
432
+ console.log(chalk5.green(` Written to ${options.output}`));
433
+ console.log("");
434
+ }
435
+ } catch (error) {
436
+ spinner.fail("Failed to generate changelog");
437
+ handleError(error);
438
+ }
439
+ });
440
+
441
+ // src/commands/health.ts
442
+ import { Command as Command4 } from "commander";
443
+ import chalk6 from "chalk";
444
+ import ora4 from "ora";
445
+ import dayjs4 from "dayjs";
446
+ var healthCommand = new Command4("health").description("Repository health score \u2014 checks README, LICENSE, CI, activity, and more").argument("[repo]", "owner/repo (default: current directory)").action(async (repoArg) => {
447
+ const spinner = ora4("Checking repository health...").start();
448
+ try {
449
+ const client = getClient();
450
+ const { owner, repo } = await resolveRepo(repoArg);
451
+ const [repoData, communityData] = await Promise.all([
452
+ client.rest.repos.get({ owner, repo }),
453
+ client.rest.repos.getCommunityProfileMetrics({ owner, repo }).catch(() => null)
454
+ ]);
455
+ const r = repoData.data;
456
+ const fileChecks = await Promise.all([
457
+ client.rest.repos.getContent({ owner, repo, path: "README.md" }).then(() => true).catch(() => false),
458
+ client.rest.repos.getContent({ owner, repo, path: "LICENSE" }).then(() => true).catch(
459
+ () => client.rest.repos.getContent({ owner, repo, path: "LICENSE.md" }).then(() => true).catch(() => false)
460
+ ),
461
+ client.rest.repos.getContent({ owner, repo, path: "CONTRIBUTING.md" }).then(() => true).catch(() => false),
462
+ client.rest.repos.getContent({ owner, repo, path: ".github/workflows" }).then(() => true).catch(() => false)
463
+ ]);
464
+ const [hasReadme, hasLicense, hasContributing, hasCi] = fileChecks;
465
+ const { data: issues } = await client.rest.issues.listForRepo({
466
+ owner,
467
+ repo,
468
+ state: "open",
469
+ per_page: 100
470
+ });
471
+ const openIssues = issues.filter((i) => !i.pull_request);
472
+ const staleDate = dayjs4().subtract(90, "day");
473
+ const staleIssues = openIssues.filter((i) => dayjs4(i.updated_at).isBefore(staleDate));
474
+ const lastCommitDate = r.pushed_at;
475
+ const daysSinceCommit = dayjs4().diff(dayjs4(lastCommitDate), "day");
476
+ const isActive = daysSinceCommit <= 30;
477
+ const hasDescription = !!r.description && r.description.length > 0;
478
+ const hasTopics = (r.topics || []).length > 0;
479
+ spinner.stop();
480
+ let score = 0;
481
+ let maxScore = 0;
482
+ const criteria = [
483
+ { ok: hasReadme, weight: 15, label: "README.md" },
484
+ { ok: hasLicense, weight: 15, label: "LICENSE" },
485
+ { ok: hasContributing, weight: 10, label: "CONTRIBUTING.md" },
486
+ { ok: hasCi, weight: 15, label: "CI/CD workflows" },
487
+ { ok: isActive, weight: 15, label: `Active (last commit ${daysSinceCommit}d ago)` },
488
+ { ok: staleIssues.length === 0, weight: 10, label: `No stale issues (${staleIssues.length} stale)` },
489
+ { ok: hasDescription, weight: 10, label: "Repository description" },
490
+ { ok: hasTopics, weight: 10, label: "Topics/tags" }
491
+ ];
492
+ for (const c of criteria) {
493
+ maxScore += c.weight;
494
+ if (c.ok) score += c.weight;
495
+ }
496
+ const pct = Math.round(score / maxScore * 100);
497
+ const grade = scoreGrade(pct);
498
+ const color = scoreColor(pct);
499
+ console.log("");
500
+ console.log(chalk6.bold(` Health Report: ${owner}/${repo}`));
501
+ console.log("");
502
+ for (const c of criteria) {
503
+ console.log(check(c.ok, c.label));
504
+ }
505
+ console.log("");
506
+ console.log(separator());
507
+ console.log("");
508
+ console.log(color(` Score: ${pct}/100 (${grade})`));
509
+ console.log(` Open issues: ${openIssues.length} (${staleIssues.length} stale)`);
510
+ console.log(` Stars: ${r.stargazers_count} | Forks: ${r.forks_count}`);
511
+ console.log("");
512
+ if (pct < 70) {
513
+ console.log(chalk6.yellow(" Suggestions:"));
514
+ for (const c of criteria) {
515
+ if (!c.ok) {
516
+ console.log(chalk6.gray(` - Add ${c.label}`));
517
+ }
518
+ }
519
+ console.log("");
520
+ }
521
+ } catch (error) {
522
+ spinner.fail("Failed to check health");
523
+ handleError(error);
524
+ }
525
+ });
526
+
527
+ // src/commands/review.ts
528
+ import { Command as Command5 } from "commander";
529
+ import chalk7 from "chalk";
530
+ import ora5 from "ora";
531
+ var reviewCommand = new Command5("review").description("List pending pull requests \u2014 assigned, review requested, and yours").option("-r, --repo <owner/repo>", "Specific repo (default: all)").action(async (options) => {
532
+ const spinner = ora5("Fetching pull requests...").start();
533
+ try {
534
+ const client = getClient();
535
+ const user = await getAuthenticatedUser();
536
+ const username = user.login;
537
+ const reviewRequested = await client.rest.search.issuesAndPullRequests({
538
+ q: `is:pr is:open review-requested:${username}${options.repo ? ` repo:${options.repo}` : ""}`,
539
+ per_page: 20,
540
+ sort: "updated",
541
+ order: "desc"
542
+ });
543
+ const assigned = await client.rest.search.issuesAndPullRequests({
544
+ q: `is:pr is:open assignee:${username}${options.repo ? ` repo:${options.repo}` : ""}`,
545
+ per_page: 20,
546
+ sort: "updated",
547
+ order: "desc"
548
+ });
549
+ const myPrs = await client.rest.search.issuesAndPullRequests({
550
+ q: `is:pr is:open author:${username}${options.repo ? ` repo:${options.repo}` : ""}`,
551
+ per_page: 20,
552
+ sort: "updated",
553
+ order: "desc"
554
+ });
555
+ spinner.stop();
556
+ console.log("");
557
+ console.log(chalk7.bold(` PR Dashboard for @${username}`));
558
+ console.log("");
559
+ const formatPr = (item) => {
560
+ const repo = item.repository_url?.split("/").slice(-2).join("/") || "";
561
+ const title = truncate(item.title, 55);
562
+ const age = timeAgo(item.created_at);
563
+ const updated = timeAgo(item.updated_at);
564
+ const draft = item.draft ? chalk7.gray(" [draft]") : "";
565
+ return ` #${item.number} ${title}${draft}` + chalk7.gray(` (${repo}) ${age} | updated ${updated}`);
566
+ };
567
+ if (reviewRequested.data.total_count > 0) {
568
+ console.log(chalk7.magenta.bold(` Review Requested (${reviewRequested.data.total_count})`));
569
+ for (const item of reviewRequested.data.items) {
570
+ console.log(formatPr(item));
571
+ }
572
+ console.log("");
573
+ }
574
+ if (assigned.data.total_count > 0) {
575
+ console.log(chalk7.blue.bold(` Assigned to You (${assigned.data.total_count})`));
576
+ for (const item of assigned.data.items) {
577
+ console.log(formatPr(item));
578
+ }
579
+ console.log("");
580
+ }
581
+ if (myPrs.data.total_count > 0) {
582
+ console.log(chalk7.green.bold(` Your PRs (${myPrs.data.total_count})`));
583
+ for (const item of myPrs.data.items) {
584
+ console.log(formatPr(item));
585
+ }
586
+ console.log("");
587
+ }
588
+ const total = reviewRequested.data.total_count + assigned.data.total_count + myPrs.data.total_count;
589
+ if (total === 0) {
590
+ console.log(chalk7.gray(" No pending pull requests. You're all caught up!"));
591
+ console.log("");
592
+ }
593
+ console.log(separator());
594
+ console.log(chalk7.bold(` ${reviewRequested.data.total_count} to review | ${assigned.data.total_count} assigned | ${myPrs.data.total_count} yours`));
595
+ console.log("");
596
+ } catch (error) {
597
+ spinner.fail("Failed to fetch PRs");
598
+ handleError(error);
599
+ }
600
+ });
601
+
602
+ // src/commands/digest.ts
603
+ import { Command as Command6 } from "commander";
604
+ import chalk8 from "chalk";
605
+ import ora6 from "ora";
606
+ import dayjs5 from "dayjs";
607
+ var digestCommand = new Command6("digest").description("Weekly/daily repository digest \u2014 commits, issues, PRs, releases").argument("[repo]", "owner/repo (default: current directory)").option("-d, --days <number>", "Look back N days", "7").action(async (repoArg, options) => {
608
+ const spinner = ora6("Building digest...").start();
609
+ try {
610
+ const client = getClient();
611
+ const { owner, repo } = await resolveRepo(repoArg);
612
+ const days = parseInt(options.days);
613
+ const since = dayjs5().subtract(days, "day").toISOString();
614
+ const [repoData, commits, issuesOpened, issuesClosed, pulls, releases] = await Promise.all([
615
+ client.rest.repos.get({ owner, repo }),
616
+ client.rest.repos.listCommits({ owner, repo, since, per_page: 100 }),
617
+ client.rest.issues.listForRepo({ owner, repo, state: "open", since, per_page: 100 }),
618
+ client.rest.issues.listForRepo({ owner, repo, state: "closed", since, per_page: 100 }),
619
+ client.rest.pulls.list({ owner, repo, state: "all", sort: "updated", direction: "desc", per_page: 50 }),
620
+ client.rest.repos.listReleases({ owner, repo, per_page: 5 })
621
+ ]);
622
+ const recentPulls = pulls.data.filter((p) => dayjs5(p.updated_at).isAfter(since));
623
+ const mergedPulls = recentPulls.filter((p) => p.merged_at && dayjs5(p.merged_at).isAfter(since));
624
+ const openedPulls = recentPulls.filter((p) => dayjs5(p.created_at).isAfter(since));
625
+ const newIssues = issuesOpened.data.filter((i) => !i.pull_request && dayjs5(i.created_at).isAfter(since));
626
+ const closedIssues = issuesClosed.data.filter((i) => !i.pull_request && dayjs5(i.closed_at).isAfter(since));
627
+ const recentReleases = releases.data.filter((r) => dayjs5(r.published_at).isAfter(since));
628
+ const contributors = /* @__PURE__ */ new Set();
629
+ for (const c of commits.data) {
630
+ const login = c.author?.login;
631
+ if (login) contributors.add(login);
632
+ }
633
+ spinner.stop();
634
+ const period = days === 1 ? "Daily" : days === 7 ? "Weekly" : `${days}-day`;
635
+ console.log("");
636
+ console.log(chalk8.bold(` ${period} Digest: ${owner}/${repo}`));
637
+ console.log(chalk8.gray(` ${dayjs5(since).format("MMM D")} - ${dayjs5().format("MMM D, YYYY")}`));
638
+ console.log("");
639
+ console.log(chalk8.cyan.bold(" Commits"));
640
+ console.log(` ${pluralize(commits.data.length, "commit")} by ${pluralize(contributors.size, "contributor")}`);
641
+ if (commits.data.length > 0) {
642
+ console.log("");
643
+ for (const c of commits.data.slice(0, 10)) {
644
+ const sha = c.sha.slice(0, 7);
645
+ const msg = c.commit.message.split("\n")[0].slice(0, 60);
646
+ const author = c.author?.login || "unknown";
647
+ console.log(chalk8.gray(` ${sha}`) + ` ${msg}` + chalk8.gray(` \u2014 ${author}`));
648
+ }
649
+ if (commits.data.length > 10) {
650
+ console.log(chalk8.gray(` ... and ${commits.data.length - 10} more`));
651
+ }
652
+ }
653
+ console.log("");
654
+ console.log(chalk8.green.bold(" Pull Requests"));
655
+ console.log(` ${pluralize(openedPulls.length, "opened")}, ${pluralize(mergedPulls.length, "merged")}`);
656
+ if (mergedPulls.length > 0) {
657
+ console.log("");
658
+ for (const pr of mergedPulls.slice(0, 5)) {
659
+ console.log(` * #${pr.number} ${pr.title}` + chalk8.gray(` by @${pr.user?.login}`));
660
+ }
661
+ }
662
+ console.log("");
663
+ console.log(chalk8.yellow.bold(" Issues"));
664
+ console.log(` ${pluralize(newIssues.length, "opened")}, ${pluralize(closedIssues.length, "closed")}`);
665
+ if (newIssues.length > 0) {
666
+ console.log("");
667
+ for (const issue of newIssues.slice(0, 5)) {
668
+ console.log(` + #${issue.number} ${issue.title}`);
669
+ }
670
+ }
671
+ console.log("");
672
+ if (recentReleases.length > 0) {
673
+ console.log(chalk8.magenta.bold(" Releases"));
674
+ for (const rel of recentReleases) {
675
+ console.log(` ${rel.tag_name} ${rel.name || ""}` + chalk8.gray(` \u2014 ${dayjs5(rel.published_at).format("MMM D")}`));
676
+ }
677
+ console.log("");
678
+ }
679
+ if (contributors.size > 0) {
680
+ console.log(chalk8.blue.bold(" Active Contributors"));
681
+ console.log(` ${[...contributors].join(", ")}`);
682
+ console.log("");
683
+ }
684
+ console.log(separator());
685
+ console.log(chalk8.bold(` ${commits.data.length} commits | ${mergedPulls.length} merged PRs | ${closedIssues.length} closed issues`));
686
+ console.log("");
687
+ } catch (error) {
688
+ spinner.fail("Failed to build digest");
689
+ handleError(error);
690
+ }
691
+ });
692
+
693
+ // src/index.ts
694
+ var program = new Command7();
695
+ program.name("gpulse").description("The Missing GitHub CLI Toolkit").version("1.0.0");
696
+ program.addCommand(standupCommand);
697
+ program.addCommand(statsCommand);
698
+ program.addCommand(changelogCommand);
699
+ program.addCommand(healthCommand);
700
+ program.addCommand(reviewCommand);
701
+ program.addCommand(digestCommand);
702
+ program.parse();