gsd-pi 2.3.5 → 2.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -12
- package/dist/cli.js +24 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +5 -3
- package/src/resources/extensions/gsd/worktree.ts +11 -0
- package/src/resources/extensions/github/formatters.ts +0 -207
- package/src/resources/extensions/github/gh-api.ts +0 -553
- package/src/resources/extensions/github/index.ts +0 -778
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# GSD 2
|
|
4
4
|
|
|
5
|
-
**The evolution of [Get Shit Done](https://github.com/
|
|
5
|
+
**The evolution of [Get Shit Done](https://github.com/gsd-build/get-shit-done) — now a real coding agent.**
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/gsd-pi)
|
|
8
8
|
[](https://www.npmjs.com/package/gsd-pi)
|
|
9
|
-
[](https://github.com/gsd-build/GSD-2)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
12
|
The original GSD went viral as a prompt framework for Claude Code. It worked, but it was fighting the tool — injecting prompts through slash commands, hoping the LLM would follow instructions, with no actual control over context windows, sessions, or execution.
|
|
@@ -122,16 +122,18 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
|
|
|
122
122
|
|
|
123
123
|
9. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/gsd auto` to resume from disk state.
|
|
124
124
|
|
|
125
|
-
###
|
|
125
|
+
### `/gsd` and `/gsd next` — Step Mode
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
By default, `/gsd` runs in **step mode**: the same state machine as auto mode, but it pauses between units with a wizard showing what completed and what's next. You advance one step at a time, review the output, and continue when ready.
|
|
128
128
|
|
|
129
129
|
- **No `.gsd/` directory** → Start a new project. Discussion flow captures your vision, constraints, and preferences.
|
|
130
130
|
- **Milestone exists, no roadmap** → Discuss or research the milestone.
|
|
131
|
-
- **Roadmap exists, slices pending** → Plan the next slice, or
|
|
131
|
+
- **Roadmap exists, slices pending** → Plan the next slice, execute one task, or switch to auto.
|
|
132
132
|
- **Mid-task** → Resume from where you left off.
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
`/gsd next` is an explicit alias for step mode. You can switch from step → auto mid-session via the wizard.
|
|
135
|
+
|
|
136
|
+
Step mode is the on-ramp. Auto mode is the highway.
|
|
135
137
|
|
|
136
138
|
---
|
|
137
139
|
|
|
@@ -170,7 +172,7 @@ gsd
|
|
|
170
172
|
|
|
171
173
|
GSD opens an interactive agent session. From there, you have two ways to work:
|
|
172
174
|
|
|
173
|
-
**`/gsd` —
|
|
175
|
+
**`/gsd` — step mode.** Type `/gsd` and GSD executes one unit of work at a time, pausing between each with a wizard showing what completed and what's next. Same state machine as auto mode, but you stay in the loop. No project yet? It starts the discussion flow. Roadmap exists? It plans or executes the next step.
|
|
174
176
|
|
|
175
177
|
**`/gsd auto` — autonomous mode.** Type `/gsd auto` and walk away. GSD researches, plans, executes, verifies, commits, and advances through every slice until the milestone is complete. Fresh context window per task. No babysitting.
|
|
176
178
|
|
|
@@ -196,13 +198,14 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
|
|
|
196
198
|
|
|
197
199
|
### First launch
|
|
198
200
|
|
|
199
|
-
On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
|
|
201
|
+
On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
|
|
200
202
|
|
|
201
203
|
### Commands
|
|
202
204
|
|
|
203
205
|
| Command | What it does |
|
|
204
206
|
|---------|-------------|
|
|
205
|
-
| `/gsd` |
|
|
207
|
+
| `/gsd` | Step mode — executes one unit at a time, pauses between each |
|
|
208
|
+
| `/gsd next` | Explicit step mode (same as bare `/gsd`) |
|
|
206
209
|
| `/gsd auto` | Autonomous mode — researches, plans, executes, commits, repeats |
|
|
207
210
|
| `/gsd stop` | Stop auto mode gracefully |
|
|
208
211
|
| `/gsd discuss` | Discuss architecture and decisions (works alongside auto mode) |
|
|
@@ -211,7 +214,13 @@ On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) f
|
|
|
211
214
|
| `/gsd prefs` | Model selection, timeouts, budget ceiling |
|
|
212
215
|
| `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
|
|
213
216
|
| `/gsd doctor` | Validate `.gsd/` integrity, find and fix issues |
|
|
217
|
+
| `/worktree` (`/wt`) | Git worktree lifecycle — create, switch, merge, remove |
|
|
218
|
+
| `/voice` | Toggle real-time speech-to-text (macOS only) |
|
|
219
|
+
| `/exit` | Kill GSD process immediately |
|
|
220
|
+
| `/clear` | Start a new session (alias for `/new`) |
|
|
214
221
|
| `Ctrl+Alt+G` | Toggle dashboard overlay |
|
|
222
|
+
| `Ctrl+Alt+V` | Toggle voice transcription |
|
|
223
|
+
| `Ctrl+Alt+B` | Show background shell processes |
|
|
215
224
|
|
|
216
225
|
---
|
|
217
226
|
|
|
@@ -311,16 +320,20 @@ budget_ceiling: 50.00
|
|
|
311
320
|
|
|
312
321
|
### Bundled Tools
|
|
313
322
|
|
|
314
|
-
GSD ships with
|
|
323
|
+
GSD ships with 13 extensions, all loaded automatically:
|
|
315
324
|
|
|
316
325
|
| Extension | What it provides |
|
|
317
326
|
|-----------|-----------------|
|
|
318
327
|
| **GSD** | Core workflow engine, auto mode, commands, dashboard |
|
|
319
328
|
| **Browser Tools** | Playwright-based browser for UI verification |
|
|
320
329
|
| **Search the Web** | Brave Search + Jina page extraction |
|
|
330
|
+
| **Google Search** | Gemini-powered web search with AI-synthesized answers |
|
|
321
331
|
| **Context7** | Up-to-date library/framework documentation |
|
|
322
332
|
| **Background Shell** | Long-running process management with readiness detection |
|
|
323
333
|
| **Subagent** | Delegated tasks with isolated context windows |
|
|
334
|
+
| **Mac Tools** | macOS native app automation via Accessibility APIs |
|
|
335
|
+
| **MCPorter** | Lazy on-demand MCP server integration |
|
|
336
|
+
| **Voice** | Real-time speech-to-text transcription (macOS) |
|
|
324
337
|
| **Slash Commands** | Custom command creation |
|
|
325
338
|
| **Ask User Questions** | Structured user input with single/multi-select |
|
|
326
339
|
| **Secure Env Collect** | Masked secret collection without manual .env editing |
|
|
@@ -345,12 +358,12 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
|
|
|
345
358
|
gsd (CLI binary)
|
|
346
359
|
└─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
|
|
347
360
|
└─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
|
|
348
|
-
├─ wizard.ts First-run API key collection (Brave/Context7/Jina)
|
|
361
|
+
├─ wizard.ts First-run API key collection (Brave/Gemini/Context7/Jina)
|
|
349
362
|
├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
|
|
350
363
|
├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
|
|
351
364
|
└─ src/resources/
|
|
352
365
|
├─ extensions/gsd/ Core GSD extension (auto, state, commands, ...)
|
|
353
|
-
├─ extensions/...
|
|
366
|
+
├─ extensions/... 12 supporting extensions
|
|
354
367
|
├─ agents/ scout, researcher, worker
|
|
355
368
|
├─ AGENTS.md Agent routing instructions
|
|
356
369
|
└─ GSD-WORKFLOW.md Manual bootstrap protocol
|
|
@@ -373,6 +386,7 @@ gsd (CLI binary)
|
|
|
373
386
|
|
|
374
387
|
Optional:
|
|
375
388
|
- Brave Search API key (web research)
|
|
389
|
+
- Google Gemini API key (web research via Gemini Search grounding)
|
|
376
390
|
- Context7 API key (library docs)
|
|
377
391
|
- Jina API key (page extraction)
|
|
378
392
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
|
|
5
5
|
import { initResources } from './resource-loader.js';
|
|
@@ -144,6 +144,29 @@ if (isPrintMode) {
|
|
|
144
144
|
const cwd = process.cwd();
|
|
145
145
|
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
146
146
|
const projectSessionsDir = join(sessionsDir, safePath);
|
|
147
|
+
// Migrate legacy flat sessions: before per-directory scoping, all .jsonl session
|
|
148
|
+
// files lived directly in ~/.gsd/sessions/. Move them into the correct per-cwd
|
|
149
|
+
// subdirectory so /resume can find them.
|
|
150
|
+
if (existsSync(sessionsDir)) {
|
|
151
|
+
try {
|
|
152
|
+
const entries = readdirSync(sessionsDir);
|
|
153
|
+
const flatJsonl = entries.filter(f => f.endsWith('.jsonl'));
|
|
154
|
+
if (flatJsonl.length > 0) {
|
|
155
|
+
const { mkdirSync } = await import('node:fs');
|
|
156
|
+
mkdirSync(projectSessionsDir, { recursive: true });
|
|
157
|
+
for (const file of flatJsonl) {
|
|
158
|
+
const src = join(sessionsDir, file);
|
|
159
|
+
const dst = join(projectSessionsDir, file);
|
|
160
|
+
if (!existsSync(dst)) {
|
|
161
|
+
renameSync(src, dst);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Non-fatal — don't block startup if migration fails
|
|
168
|
+
}
|
|
169
|
+
}
|
|
147
170
|
const sessionManager = SessionManager.create(cwd, projectSessionsDir);
|
|
148
171
|
initResources(agentDir);
|
|
149
172
|
const resourceLoader = new DefaultResourceLoader({ agentDir });
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -44,10 +44,12 @@ try {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Install Playwright chromium for browser tools (non-fatal)
|
|
47
|
-
const args = os.platform() === 'linux' ? '--with-deps' : ''
|
|
48
47
|
try {
|
|
49
|
-
execSync(
|
|
48
|
+
execSync('npx playwright install chromium', { stdio: 'inherit' })
|
|
50
49
|
process.stderr.write(`\n ${green}✓${reset} Browser tools ready\n\n`)
|
|
51
50
|
} catch {
|
|
52
|
-
|
|
51
|
+
const hint = os.platform() === 'linux'
|
|
52
|
+
? `${cyan}npx playwright install --with-deps chromium${reset}`
|
|
53
|
+
: `${cyan}npx playwright install chromium${reset}`
|
|
54
|
+
process.stderr.write(`\n ${yellow}⚠${reset} Browser tools unavailable — run ${hint} to enable\n\n`)
|
|
53
55
|
}
|
|
@@ -86,6 +86,17 @@ export function ensureSliceBranch(basePath: string, milestoneId: string, sliceId
|
|
|
86
86
|
created = true;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Auto-commit dirty files before checkout to prevent "would be overwritten" errors.
|
|
90
|
+
// This handles cases where doctor, STATE.md rebuild, or agent work left uncommitted changes.
|
|
91
|
+
const status = runGit(basePath, ["status", "--short"]);
|
|
92
|
+
if (status.trim()) {
|
|
93
|
+
runGit(basePath, ["add", "-A"]);
|
|
94
|
+
const staged = runGit(basePath, ["diff", "--cached", "--stat"]);
|
|
95
|
+
if (staged.trim()) {
|
|
96
|
+
runGit(basePath, ["commit", "-m", `"chore: auto-commit before switching to ${branch}"`]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
runGit(basePath, ["checkout", branch]);
|
|
90
101
|
return created;
|
|
91
102
|
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formatters — produce text summaries for issues, PRs, comments, etc.
|
|
3
|
-
*
|
|
4
|
-
* Used by both tools (LLM context) and renderers (TUI display).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { GhIssue, GhPullRequest, GhComment, GhReview, GhLabel, GhMilestone } from "./gh-api.js";
|
|
8
|
-
|
|
9
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
function timeAgo(dateStr: string): string {
|
|
12
|
-
const now = Date.now();
|
|
13
|
-
const then = new Date(dateStr).getTime();
|
|
14
|
-
const diff = now - then;
|
|
15
|
-
const mins = Math.floor(diff / 60000);
|
|
16
|
-
if (mins < 1) return "just now";
|
|
17
|
-
if (mins < 60) return `${mins}m ago`;
|
|
18
|
-
const hours = Math.floor(mins / 60);
|
|
19
|
-
if (hours < 24) return `${hours}h ago`;
|
|
20
|
-
const days = Math.floor(hours / 24);
|
|
21
|
-
if (days < 30) return `${days}d ago`;
|
|
22
|
-
const months = Math.floor(days / 30);
|
|
23
|
-
if (months < 12) return `${months}mo ago`;
|
|
24
|
-
return `${Math.floor(months / 12)}y ago`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stateIcon(state: string, draft?: boolean): string {
|
|
28
|
-
if (draft) return "◇";
|
|
29
|
-
switch (state) {
|
|
30
|
-
case "open":
|
|
31
|
-
return "●";
|
|
32
|
-
case "closed":
|
|
33
|
-
return "✓";
|
|
34
|
-
case "merged":
|
|
35
|
-
return "⊕";
|
|
36
|
-
default:
|
|
37
|
-
return "○";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function truncateBody(body: string | null, maxLines = 10): string {
|
|
42
|
-
if (!body) return "(no description)";
|
|
43
|
-
const lines = body.split("\n");
|
|
44
|
-
if (lines.length <= maxLines) return body;
|
|
45
|
-
return lines.slice(0, maxLines).join("\n") + `\n... (${lines.length - maxLines} more lines)`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ─── Issue formatting ─────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
export function formatIssueOneLiner(issue: GhIssue): string {
|
|
51
|
-
const icon = stateIcon(issue.state);
|
|
52
|
-
const labels = issue.labels.map((l) => l.name).join(", ");
|
|
53
|
-
const labelStr = labels ? ` [${labels}]` : "";
|
|
54
|
-
const assignee = issue.assignees.length ? ` → ${issue.assignees.map((a) => a.login).join(", ")}` : "";
|
|
55
|
-
return `${icon} #${issue.number} ${issue.title}${labelStr}${assignee} (${timeAgo(issue.updated_at)})`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function formatIssueDetail(issue: GhIssue): string {
|
|
59
|
-
const lines: string[] = [];
|
|
60
|
-
lines.push(`# Issue #${issue.number}: ${issue.title}`);
|
|
61
|
-
lines.push(`State: ${issue.state} | Author: @${issue.user.login} | Created: ${timeAgo(issue.created_at)} | Updated: ${timeAgo(issue.updated_at)}`);
|
|
62
|
-
|
|
63
|
-
if (issue.assignees.length) {
|
|
64
|
-
lines.push(`Assignees: ${issue.assignees.map((a) => `@${a.login}`).join(", ")}`);
|
|
65
|
-
}
|
|
66
|
-
if (issue.labels.length) {
|
|
67
|
-
lines.push(`Labels: ${issue.labels.map((l) => l.name).join(", ")}`);
|
|
68
|
-
}
|
|
69
|
-
if (issue.milestone) {
|
|
70
|
-
lines.push(`Milestone: ${issue.milestone.title}`);
|
|
71
|
-
}
|
|
72
|
-
lines.push(`Comments: ${issue.comments}`);
|
|
73
|
-
lines.push(`URL: ${issue.html_url}`);
|
|
74
|
-
lines.push("");
|
|
75
|
-
lines.push(truncateBody(issue.body, 30));
|
|
76
|
-
return lines.join("\n");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function formatIssueList(issues: GhIssue[]): string {
|
|
80
|
-
if (!issues.length) return "No issues found.";
|
|
81
|
-
return issues.map(formatIssueOneLiner).join("\n");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ─── PR formatting ────────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
export function formatPROneLiner(pr: GhPullRequest): string {
|
|
87
|
-
const icon = stateIcon(pr.merged_at ? "merged" : pr.state, pr.draft);
|
|
88
|
-
const labels = pr.labels.map((l) => l.name).join(", ");
|
|
89
|
-
const labelStr = labels ? ` [${labels}]` : "";
|
|
90
|
-
const draftStr = pr.draft ? " (draft)" : "";
|
|
91
|
-
const reviewers = pr.requested_reviewers.map((r) => r.login).join(", ");
|
|
92
|
-
const reviewerStr = reviewers ? ` ⟵ ${reviewers}` : "";
|
|
93
|
-
return `${icon} #${pr.number} ${pr.title}${draftStr}${labelStr}${reviewerStr} (${timeAgo(pr.updated_at)})`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function formatPRDetail(pr: GhPullRequest): string {
|
|
97
|
-
const lines: string[] = [];
|
|
98
|
-
const mergedState = pr.merged_at ? "merged" : pr.state;
|
|
99
|
-
lines.push(`# PR #${pr.number}: ${pr.title}`);
|
|
100
|
-
lines.push(`State: ${mergedState}${pr.draft ? " (draft)" : ""} | Author: @${pr.user.login} | Created: ${timeAgo(pr.created_at)} | Updated: ${timeAgo(pr.updated_at)}`);
|
|
101
|
-
lines.push(`Branch: ${pr.head.ref} → ${pr.base.ref}`);
|
|
102
|
-
|
|
103
|
-
if (pr.assignees.length) {
|
|
104
|
-
lines.push(`Assignees: ${pr.assignees.map((a) => `@${a.login}`).join(", ")}`);
|
|
105
|
-
}
|
|
106
|
-
if (pr.labels.length) {
|
|
107
|
-
lines.push(`Labels: ${pr.labels.map((l) => l.name).join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
if (pr.milestone) {
|
|
110
|
-
lines.push(`Milestone: ${pr.milestone.title}`);
|
|
111
|
-
}
|
|
112
|
-
if (pr.requested_reviewers.length) {
|
|
113
|
-
lines.push(`Reviewers: ${pr.requested_reviewers.map((r) => `@${r.login}`).join(", ")}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
lines.push(`Mergeable: ${pr.mergeable === null ? "checking..." : pr.mergeable ? "yes" : "no"} (${pr.mergeable_state})`);
|
|
117
|
-
lines.push(`Comments: ${pr.comments} | Review comments: ${pr.review_comments}`);
|
|
118
|
-
lines.push(`URL: ${pr.html_url}`);
|
|
119
|
-
lines.push("");
|
|
120
|
-
lines.push(truncateBody(pr.body, 30));
|
|
121
|
-
return lines.join("\n");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function formatPRList(prs: GhPullRequest[]): string {
|
|
125
|
-
if (!prs.length) return "No pull requests found.";
|
|
126
|
-
return prs.map(formatPROneLiner).join("\n");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ─── Comment formatting ──────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
export function formatComment(comment: GhComment): string {
|
|
132
|
-
return `@${comment.user.login} (${timeAgo(comment.created_at)}):\n${truncateBody(comment.body, 8)}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function formatCommentList(comments: GhComment[]): string {
|
|
136
|
-
if (!comments.length) return "No comments.";
|
|
137
|
-
return comments.map(formatComment).join("\n\n---\n\n");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ─── Review formatting ───────────────────────────────────────────────────────
|
|
141
|
-
|
|
142
|
-
function reviewStateIcon(state: string): string {
|
|
143
|
-
switch (state) {
|
|
144
|
-
case "APPROVED":
|
|
145
|
-
return "✓";
|
|
146
|
-
case "CHANGES_REQUESTED":
|
|
147
|
-
return "✗";
|
|
148
|
-
case "COMMENTED":
|
|
149
|
-
return "💬";
|
|
150
|
-
case "DISMISSED":
|
|
151
|
-
return "—";
|
|
152
|
-
case "PENDING":
|
|
153
|
-
return "…";
|
|
154
|
-
default:
|
|
155
|
-
return "?";
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function formatReview(review: GhReview): string {
|
|
160
|
-
const icon = reviewStateIcon(review.state);
|
|
161
|
-
const body = review.body ? `\n${truncateBody(review.body, 5)}` : "";
|
|
162
|
-
return `${icon} @${review.user.login}: ${review.state} (${timeAgo(review.submitted_at)})${body}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function formatReviewList(reviews: GhReview[]): string {
|
|
166
|
-
if (!reviews.length) return "No reviews.";
|
|
167
|
-
return reviews.map(formatReview).join("\n\n");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ─── Label / Milestone formatting ─────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
export function formatLabel(label: GhLabel): string {
|
|
173
|
-
const desc = label.description ? ` — ${label.description}` : "";
|
|
174
|
-
return `• ${label.name} (#${label.color})${desc}`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function formatLabelList(labels: GhLabel[]): string {
|
|
178
|
-
if (!labels.length) return "No labels.";
|
|
179
|
-
return labels.map(formatLabel).join("\n");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function formatMilestone(ms: GhMilestone): string {
|
|
183
|
-
const progress = ms.open_issues + ms.closed_issues > 0 ? Math.round((ms.closed_issues / (ms.open_issues + ms.closed_issues)) * 100) : 0;
|
|
184
|
-
const due = ms.due_on ? ` | Due: ${new Date(ms.due_on).toISOString().split("T")[0]}` : "";
|
|
185
|
-
return `• ${ms.title} (${ms.state}) — ${progress}% complete (${ms.closed_issues}/${ms.open_issues + ms.closed_issues})${due}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function formatMilestoneList(milestones: GhMilestone[]): string {
|
|
189
|
-
if (!milestones.length) return "No milestones.";
|
|
190
|
-
return milestones.map(formatMilestone).join("\n");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ─── File change formatting ───────────────────────────────────────────────────
|
|
194
|
-
|
|
195
|
-
export function formatFileChanges(
|
|
196
|
-
files: { filename: string; status: string; additions: number; deletions: number; changes: number }[],
|
|
197
|
-
): string {
|
|
198
|
-
if (!files.length) return "No files changed.";
|
|
199
|
-
const lines = files.map((f) => {
|
|
200
|
-
const statusIcon = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
|
|
201
|
-
return `${statusIcon} ${f.filename} (+${f.additions} -${f.deletions})`;
|
|
202
|
-
});
|
|
203
|
-
const totalAdd = files.reduce((s, f) => s + f.additions, 0);
|
|
204
|
-
const totalDel = files.reduce((s, f) => s + f.deletions, 0);
|
|
205
|
-
lines.push(`\n${files.length} files changed, +${totalAdd} -${totalDel}`);
|
|
206
|
-
return lines.join("\n");
|
|
207
|
-
}
|