gsd-pi 2.34.0-dev.0150ae9 → 2.34.0-dev.7d38042

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,213 @@
1
+ /**
2
+ * GSD Changelog — Fetch and display categorized release notes from GitHub
3
+ *
4
+ * Fetches releases from the gsd-build/gsd-2 GitHub repository,
5
+ * prompts the user for a version filter, and sends raw release notes
6
+ * into the conversation for the LLM to summarize.
7
+ *
8
+ * Entry point: handleChangelog() called from commands.ts
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
12
+
13
+ // ─── Types ────────────────────────────────────────────────────────────────────
14
+
15
+ interface GitHubRelease {
16
+ tag_name: string;
17
+ name: string;
18
+ body: string;
19
+ }
20
+
21
+ // ─── Semver comparison ────────────────────────────────────────────────────────
22
+
23
+ function compareSemver(a: string, b: string): number {
24
+ const pa = a.split(".").map(Number);
25
+ const pb = b.split(".").map(Number);
26
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
27
+ const va = pa[i] || 0;
28
+ const vb = pb[i] || 0;
29
+ if (va > vb) return 1;
30
+ if (va < vb) return -1;
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ function stripV(tag: string): string {
36
+ return tag.startsWith("v") ? tag.slice(1) : tag;
37
+ }
38
+
39
+ // ─── Body parsing ─────────────────────────────────────────────────────────────
40
+
41
+ interface CategorySection {
42
+ heading: string;
43
+ content: string;
44
+ }
45
+
46
+ function parseReleaseBody(body: string): CategorySection[] {
47
+ if (!body) return [];
48
+
49
+ const sections: CategorySection[] = [];
50
+ const lines = body.split("\n");
51
+ let currentHeading: string | null = null;
52
+ let currentLines: string[] = [];
53
+
54
+ for (const line of lines) {
55
+ if (line.startsWith("### ")) {
56
+ if (currentHeading !== null) {
57
+ const content = currentLines.join("\n").trim();
58
+ if (content) {
59
+ sections.push({ heading: currentHeading, content });
60
+ }
61
+ }
62
+ currentHeading = line.slice(4).trim();
63
+ currentLines = [];
64
+ } else if (currentHeading !== null) {
65
+ currentLines.push(line);
66
+ }
67
+ }
68
+
69
+ if (currentHeading !== null) {
70
+ const content = currentLines.join("\n").trim();
71
+ if (content) {
72
+ sections.push({ heading: currentHeading, content });
73
+ }
74
+ }
75
+
76
+ return sections;
77
+ }
78
+
79
+ // ─── Display formatting ──────────────────────────────────────────────────────
80
+
81
+ function formatRelease(release: GitHubRelease): string {
82
+ const version = stripV(release.tag_name);
83
+ const title = release.name || `v${version}`;
84
+ const sections = parseReleaseBody(release.body);
85
+
86
+ const parts: string[] = [`## ${title}`];
87
+
88
+ if (sections.length === 0) {
89
+ if (release.body?.trim()) {
90
+ parts.push(release.body.trim());
91
+ } else {
92
+ parts.push("_No release notes._");
93
+ }
94
+ } else {
95
+ for (const section of sections) {
96
+ parts.push(`### ${section.heading}`);
97
+ parts.push(section.content);
98
+ }
99
+ }
100
+
101
+ return parts.join("\n\n");
102
+ }
103
+
104
+ // ─── Entry Point ──────────────────────────────────────────────────────────────
105
+
106
+ const RELEASES_URL = "https://api.github.com/repos/gsd-build/gsd-2/releases?per_page=100";
107
+
108
+ export async function handleChangelog(
109
+ args: string,
110
+ ctx: ExtensionCommandContext,
111
+ pi: ExtensionAPI,
112
+ ): Promise<void> {
113
+ // ── Fetch releases ──────────────────────────────────────────────────────
114
+ let releases: GitHubRelease[];
115
+ try {
116
+ const response = await fetch(RELEASES_URL, {
117
+ headers: { "User-Agent": "gsd-changelog" },
118
+ });
119
+
120
+ if (!response.ok) {
121
+ ctx.ui.notify(
122
+ `Failed to fetch changelog: GitHub API returned ${response.status} ${response.statusText}`,
123
+ "error",
124
+ );
125
+ return;
126
+ }
127
+
128
+ releases = (await response.json()) as GitHubRelease[];
129
+ } catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err);
131
+ ctx.ui.notify(`Failed to fetch changelog: ${message}`, "error");
132
+ return;
133
+ }
134
+
135
+ if (!releases.length) {
136
+ ctx.ui.notify("No releases found in the repository.", "warning");
137
+ return;
138
+ }
139
+
140
+ // ── Determine version filter ────────────────────────────────────────────
141
+ const currentVersion = process.env.GSD_VERSION || "";
142
+ let sinceVersion: string | undefined;
143
+ let showCurrentOnly = false;
144
+
145
+ if (args.trim()) {
146
+ sinceVersion = stripV(args.trim());
147
+ } else {
148
+ const input = await ctx.ui.input(
149
+ "Show changes since version:",
150
+ currentVersion || "latest",
151
+ );
152
+
153
+ if (input === undefined) {
154
+ return;
155
+ }
156
+
157
+ if (input.trim() === "") {
158
+ showCurrentOnly = true;
159
+ } else {
160
+ sinceVersion = stripV(input.trim());
161
+ }
162
+ }
163
+
164
+ // ── Filter releases ─────────────────────────────────────────────────────
165
+ let matched: GitHubRelease[];
166
+
167
+ if (showCurrentOnly) {
168
+ if (!currentVersion) {
169
+ ctx.ui.notify(
170
+ "GSD_VERSION is not set — cannot determine current release. Provide a version instead.",
171
+ "warning",
172
+ );
173
+ return;
174
+ }
175
+ const found = releases.find((r) => stripV(r.tag_name) === currentVersion);
176
+ if (!found) {
177
+ ctx.ui.notify(`No release found matching current version v${currentVersion}`, "warning");
178
+ return;
179
+ }
180
+ matched = [found];
181
+ } else if (sinceVersion) {
182
+ matched = releases
183
+ .filter((r) => compareSemver(stripV(r.tag_name), sinceVersion!) > 0)
184
+ .sort((a, b) => compareSemver(stripV(b.tag_name), stripV(a.tag_name)));
185
+
186
+ if (!matched.length) {
187
+ ctx.ui.notify(`No releases found since v${sinceVersion}`, "warning");
188
+ return;
189
+ }
190
+ } else {
191
+ matched = [releases[0]];
192
+ }
193
+
194
+ // ── Send to LLM for summarization ───────────────────────────────────────
195
+ const rawOutput = matched.map(formatRelease).join("\n\n---\n\n");
196
+ const versionRange = sinceVersion
197
+ ? `since v${sinceVersion} (${matched.length} release${matched.length === 1 ? "" : "s"})`
198
+ : `for current release ${matched[0].name || matched[0].tag_name}`;
199
+
200
+ const prompt = [
201
+ `Here are the raw GSD changelog entries ${versionRange}.`,
202
+ "Summarize the most important changes — group by category (Added, Changed, Fixed, etc.),",
203
+ "keep only the most impactful items (max 5 per category), skip trivial changes,",
204
+ "and include the version where each item appeared. Keep it concise and scannable.",
205
+ "",
206
+ rawOutput,
207
+ ].join("\n");
208
+
209
+ pi.sendMessage(
210
+ { customType: "gsd-changelog", content: prompt, display: true },
211
+ { triggerTurn: true },
212
+ );
213
+ }
@@ -12,6 +12,7 @@ const TOP_LEVEL_SUBCOMMANDS = [
12
12
  { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
13
13
  { cmd: "discuss", desc: "Discuss architecture and decisions" },
14
14
  { cmd: "capture", desc: "Fire-and-forget thought capture" },
15
+ { cmd: "changelog", desc: "Show categorized release notes" },
15
16
  { cmd: "triage", desc: "Manually trigger triage of pending captures" },
16
17
  { cmd: "dispatch", desc: "Dispatch a specific phase directly" },
17
18
  { cmd: "history", desc: "View execution history" },
@@ -71,7 +71,7 @@ export function projectRoot(): string {
71
71
 
72
72
  export function registerGSDCommand(pi: ExtensionAPI): void {
73
73
  pi.registerCommand("gsd", {
74
- description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
74
+ description: "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|update",
75
75
  getArgumentCompletions: (prefix: string) => {
76
76
  const subcommands = [
77
77
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -85,6 +85,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
85
85
  { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
86
86
  { cmd: "discuss", desc: "Discuss architecture and decisions" },
87
87
  { cmd: "capture", desc: "Fire-and-forget thought capture" },
88
+ { cmd: "changelog", desc: "Show categorized release notes" },
88
89
  { cmd: "triage", desc: "Manually trigger triage of pending captures" },
89
90
  { cmd: "dispatch", desc: "Dispatch a specific phase directly" },
90
91
  { cmd: "history", desc: "View execution history" },
@@ -499,6 +500,12 @@ export async function handleGSDCommand(
499
500
  return;
500
501
  }
501
502
 
503
+ if (trimmed === "changelog" || trimmed.startsWith("changelog ")) {
504
+ const { handleChangelog } = await import("./changelog.js");
505
+ await handleChangelog(trimmed.replace(/^changelog\s*/, "").trim(), ctx, pi);
506
+ return;
507
+ }
508
+
502
509
  if (trimmed === "next" || trimmed.startsWith("next ")) {
503
510
  if (trimmed.includes("--dry-run")) {
504
511
  await handleDryRun(ctx, projectRoot());
@@ -928,6 +935,7 @@ function showHelp(ctx: ExtensionCommandContext): void {
928
935
  " /gsd visualize Interactive 10-tab TUI (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)",
929
936
  " /gsd queue Show queued/dispatched units and execution order",
930
937
  " /gsd history View execution history [--cost] [--phase] [--model] [N]",
938
+ " /gsd changelog Show categorized release notes [version]",
931
939
  "",
932
940
  "COURSE CORRECTION",
933
941
  " /gsd steer <desc> Apply user override to active work",