gsd-pi 2.28.0-dev.4009980 → 2.28.0-dev.b23c118

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.
@@ -4,6 +4,7 @@
4
4
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
5
5
  import { writeFileSync, mkdirSync } from "node:fs";
6
6
  import { join, basename } from "node:path";
7
+ import { exec } from "node:child_process";
7
8
  import {
8
9
  getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
9
10
  aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
@@ -12,6 +13,28 @@ import type { UnitMetrics } from "./metrics.js";
12
13
  import { gsdRoot } from "./paths.js";
13
14
  import { formatDuration, fileLink } from "../shared/mod.js";
14
15
 
16
+ /**
17
+ * Open a file in the user's default browser.
18
+ * Uses platform-specific commands: `open` (macOS), `xdg-open` (Linux), `start` (Windows).
19
+ * Non-blocking, non-fatal — failures are silently ignored.
20
+ */
21
+ export function openInBrowser(filePath: string): void {
22
+ const cmd =
23
+ process.platform === "darwin" ? "open" :
24
+ process.platform === "win32" ? "start" :
25
+ "xdg-open";
26
+
27
+ // On Windows, `start` needs an empty title argument when the path has spaces
28
+ const args = process.platform === "win32"
29
+ ? `"" "${filePath}"`
30
+ : `"${filePath}"`;
31
+
32
+ exec(`${cmd} ${args}`, (err) => {
33
+ // Non-fatal — if the browser can't be opened, the file path is still shown
34
+ if (err) void err;
35
+ });
36
+ }
37
+
15
38
  /**
16
39
  * Write an export file directly, without requiring an ExtensionCommandContext.
17
40
  * Used by the visualizer overlay export tab.
@@ -167,10 +190,12 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
167
190
  paths.push(bn(outPath));
168
191
  }
169
192
 
193
+ const indexPath = join(gsdRoot(basePath), "reports", "index.html");
170
194
  ctx.ui.notify(
171
- `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nBrowse all reports: .gsd/reports/index.html`,
195
+ `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nOpening reports index in browser...`,
172
196
  "success",
173
197
  );
198
+ openInBrowser(indexPath);
174
199
  } else {
175
200
  // Single report for the active milestone (existing behavior)
176
201
  const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
@@ -194,9 +219,10 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
194
219
  phase: data.phase,
195
220
  });
196
221
  ctx.ui.notify(
197
- `HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`,
222
+ `HTML report saved: .gsd/reports/${bn(outPath)}\nOpening in browser...`,
198
223
  "success",
199
224
  );
225
+ openInBrowser(outPath);
200
226
  }
201
227
  } catch (err) {
202
228
  ctx.ui.notify(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.28.0-dev.4009980",
3
+ "version": "2.28.0-dev.b23c118",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "configDir": ".gsd"
36
36
  },
37
37
  "engines": {
38
- "node": ">=20.6.0"
38
+ "node": ">=22.0.0"
39
39
  },
40
40
  "packageManager": "npm@10.9.3",
41
41
  "scripts": {
@@ -117,7 +117,7 @@
117
117
  "zod-to-json-schema": "^3.24.6"
118
118
  },
119
119
  "devDependencies": {
120
- "@types/node": "^22.0.0",
120
+ "@types/node": "^24.12.0",
121
121
  "@types/picomatch": "^4.0.2",
122
122
  "c8": "^11.0.0",
123
123
  "jiti": "^2.6.1",
@@ -4,6 +4,7 @@
4
4
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
5
5
  import { writeFileSync, mkdirSync } from "node:fs";
6
6
  import { join, basename } from "node:path";
7
+ import { exec } from "node:child_process";
7
8
  import {
8
9
  getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
9
10
  aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
@@ -12,6 +13,28 @@ import type { UnitMetrics } from "./metrics.js";
12
13
  import { gsdRoot } from "./paths.js";
13
14
  import { formatDuration, fileLink } from "../shared/mod.js";
14
15
 
16
+ /**
17
+ * Open a file in the user's default browser.
18
+ * Uses platform-specific commands: `open` (macOS), `xdg-open` (Linux), `start` (Windows).
19
+ * Non-blocking, non-fatal — failures are silently ignored.
20
+ */
21
+ export function openInBrowser(filePath: string): void {
22
+ const cmd =
23
+ process.platform === "darwin" ? "open" :
24
+ process.platform === "win32" ? "start" :
25
+ "xdg-open";
26
+
27
+ // On Windows, `start` needs an empty title argument when the path has spaces
28
+ const args = process.platform === "win32"
29
+ ? `"" "${filePath}"`
30
+ : `"${filePath}"`;
31
+
32
+ exec(`${cmd} ${args}`, (err) => {
33
+ // Non-fatal — if the browser can't be opened, the file path is still shown
34
+ if (err) void err;
35
+ });
36
+ }
37
+
15
38
  /**
16
39
  * Write an export file directly, without requiring an ExtensionCommandContext.
17
40
  * Used by the visualizer overlay export tab.
@@ -167,10 +190,12 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
167
190
  paths.push(bn(outPath));
168
191
  }
169
192
 
193
+ const indexPath = join(gsdRoot(basePath), "reports", "index.html");
170
194
  ctx.ui.notify(
171
- `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nBrowse all reports: .gsd/reports/index.html`,
195
+ `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nOpening reports index in browser...`,
172
196
  "success",
173
197
  );
198
+ openInBrowser(indexPath);
174
199
  } else {
175
200
  // Single report for the active milestone (existing behavior)
176
201
  const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
@@ -194,9 +219,10 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
194
219
  phase: data.phase,
195
220
  });
196
221
  ctx.ui.notify(
197
- `HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`,
222
+ `HTML report saved: .gsd/reports/${bn(outPath)}\nOpening in browser...`,
198
223
  "success",
199
224
  );
225
+ openInBrowser(outPath);
200
226
  }
201
227
  } catch (err) {
202
228
  ctx.ui.notify(