@oh-my-pi/omp-stats 15.10.11 → 15.11.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,18 @@
1
+ /**
2
+ * Embedded stats dashboard archive handling.
3
+ *
4
+ * `embedded-client.generated.txt` holds the base64 of a gzipped tar of the
5
+ * built dashboard (`dist/client`). It is populated by
6
+ * `scripts/generate-client-bundle.ts --generate` for compiled binaries and the
7
+ * prepacked npm bundle, and reset to an empty file afterwards so the dev tree
8
+ * keeps building the dashboard from source.
9
+ */
10
+ /**
11
+ * Decode the generated archive text.
12
+ *
13
+ * Returns `null` when the content is blank or not a raw gzip archive encoded as
14
+ * base64 — notably the legacy placeholder that contained a TypeScript
15
+ * `export const … = "";` stub, which must be treated as "no archive embedded"
16
+ * rather than decoded into garbage bytes.
17
+ */
18
+ export declare function decodeEmbeddedClientArchive(txt: string): Buffer | null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "15.10.11",
4
+ "version": "15.11.0",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,9 +37,9 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-ai": "15.10.11",
41
- "@oh-my-pi/pi-catalog": "15.10.11",
42
- "@oh-my-pi/pi-utils": "15.10.11",
40
+ "@oh-my-pi/pi-ai": "15.11.0",
41
+ "@oh-my-pi/pi-catalog": "15.11.0",
42
+ "@oh-my-pi/pi-utils": "15.11.0",
43
43
  "@tailwindcss/node": "^4.3.0",
44
44
  "chart.js": "^4.5.1",
45
45
  "date-fns": "^4.3.0",
@@ -1,7 +0,0 @@
1
- /**
2
- * Embedded stats dashboard bundle for compiled binaries.
3
- *
4
- * This file is generated by `bun --cwd=packages/stats scripts/generate-client-bundle.ts --generate` during
5
- * binary builds. The checked-in value is intentionally empty.
6
- */
7
- export const EMBEDDED_CLIENT_ARCHIVE_TAR_GZ_BASE64 = "";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Embedded stats dashboard archive handling.
3
+ *
4
+ * `embedded-client.generated.txt` holds the base64 of a gzipped tar of the
5
+ * built dashboard (`dist/client`). It is populated by
6
+ * `scripts/generate-client-bundle.ts --generate` for compiled binaries and the
7
+ * prepacked npm bundle, and reset to an empty file afterwards so the dev tree
8
+ * keeps building the dashboard from source.
9
+ */
10
+
11
+ /**
12
+ * Decode the generated archive text.
13
+ *
14
+ * Returns `null` when the content is blank or not a raw gzip archive encoded as
15
+ * base64 — notably the legacy placeholder that contained a TypeScript
16
+ * `export const … = "";` stub, which must be treated as "no archive embedded"
17
+ * rather than decoded into garbage bytes.
18
+ */
19
+ export function decodeEmbeddedClientArchive(txt: string): Buffer | null {
20
+ const normalized = txt.replaceAll(/\s+/g, "");
21
+ if (!normalized) return null;
22
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) return null;
23
+ const archiveBytes = Buffer.from(normalized, "base64");
24
+ if (archiveBytes[0] !== 0x1f || archiveBytes[1] !== 0x8b) return null;
25
+ return archiveBytes;
26
+ }
package/src/server.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import type { Dirent } from "node:fs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
5
+ import { isEnoent } from "@oh-my-pi/pi-utils";
4
6
  import { $ } from "bun";
5
7
  import {
6
8
  getBehaviorDashboardStats,
@@ -14,24 +16,27 @@ import {
14
16
  getTotalMessageCount,
15
17
  syncAllSessions,
16
18
  } from "./aggregator";
19
+ import { decodeEmbeddedClientArchive } from "./embedded-client";
17
20
  import embeddedClientArchiveTxt from "./embedded-client.generated.txt";
18
21
 
19
- const getEmbeddedClientArchive = (() => {
20
- const txt = embeddedClientArchiveTxt.replaceAll(/[\s\r\n]/g, "").trim();
21
- if (!txt) return null;
22
- return () => Buffer.from(txt, "base64");
23
- })();
22
+ const EMBEDDED_CLIENT_ARCHIVE = decodeEmbeddedClientArchive(embeddedClientArchiveTxt);
24
23
 
25
24
  const CLIENT_DIR = path.join(import.meta.dir, "client");
26
25
  const STATIC_DIR = path.join(import.meta.dir, "..", "dist", "client");
27
26
  const IS_BUN_COMPILED =
28
- Bun.env.PI_COMPILED ||
27
+ Boolean(process.env.PI_COMPILED || Bun.env.PI_COMPILED) ||
29
28
  import.meta.url.includes("$bunfs") ||
30
29
  import.meta.url.includes("~BUN") ||
31
30
  import.meta.url.includes("%7EBUN");
31
+ // The prepacked npm bundle (coding-agent dist/cli.js) constant-folds
32
+ // process.env.PI_BUNDLED at build time. Like compiled binaries, it ships no
33
+ // dashboard sources or prebuilt dist/client next to the bundle, so the
34
+ // embedded archive is the only viable asset source.
35
+ const IS_PREBUILT = IS_BUN_COMPILED || Boolean(process.env.PI_BUNDLED || Bun.env.PI_BUNDLED);
36
+ const USE_EMBEDDED_CLIENT = EMBEDDED_CLIENT_ARCHIVE !== null || IS_PREBUILT;
32
37
 
33
- const COMPILED_CLIENT_DIR_ROOT = path.join(os.tmpdir(), "omp-stats-client");
34
- let compiledClientDirPromise: Promise<string> | null = null;
38
+ const EMBEDDED_CLIENT_DIR_ROOT = path.join(os.tmpdir(), "omp-stats-client");
39
+ let embeddedClientDirPromise: Promise<string> | null = null;
35
40
 
36
41
  function sanitizeArchivePath(archivePath: string): string | null {
37
42
  const normalized = archivePath.replaceAll("\\", "/").replace(/^\.\//, "");
@@ -56,18 +61,19 @@ async function extractEmbeddedClientArchive(archiveBytes: Buffer, outputDir: str
56
61
  }
57
62
  }
58
63
 
59
- async function getCompiledClientDir(): Promise<string> {
60
- if (!IS_BUN_COMPILED) return STATIC_DIR;
61
- if (compiledClientDirPromise) return compiledClientDirPromise;
64
+ async function getEmbeddedClientDir(): Promise<string> {
65
+ if (!USE_EMBEDDED_CLIENT) return STATIC_DIR;
66
+ if (embeddedClientDirPromise) return embeddedClientDirPromise;
62
67
 
63
- const archiveBytes = getEmbeddedClientArchive?.();
64
- if (!archiveBytes) {
65
- throw new Error("Compiled stats client bundle missing. Rebuild binary with embedded stats assets.");
68
+ if (!EMBEDDED_CLIENT_ARCHIVE) {
69
+ throw new Error(
70
+ "Embedded stats client bundle missing. Rebuild the omp binary or npm bundle with embedded stats assets.",
71
+ );
66
72
  }
67
73
 
68
- compiledClientDirPromise = (async () => {
69
- const bundleHash = Bun.hash(archiveBytes).toString(16);
70
- const outputDir = path.join(COMPILED_CLIENT_DIR_ROOT, bundleHash);
74
+ embeddedClientDirPromise = (async () => {
75
+ const bundleHash = Bun.hash(EMBEDDED_CLIENT_ARCHIVE).toString(16);
76
+ const outputDir = path.join(EMBEDDED_CLIENT_DIR_ROOT, bundleHash);
71
77
  const markerPath = path.join(outputDir, "index.html");
72
78
  try {
73
79
  const marker = await fs.stat(markerPath);
@@ -76,15 +82,24 @@ async function getCompiledClientDir(): Promise<string> {
76
82
 
77
83
  await fs.rm(outputDir, { recursive: true, force: true });
78
84
  await fs.mkdir(outputDir, { recursive: true });
79
- await extractEmbeddedClientArchive(archiveBytes, outputDir);
85
+ await extractEmbeddedClientArchive(EMBEDDED_CLIENT_ARCHIVE, outputDir);
80
86
  return outputDir;
81
87
  })();
82
88
 
83
- return compiledClientDirPromise;
89
+ return embeddedClientDirPromise;
84
90
  }
85
91
 
86
92
  async function getLatestMtime(dir: string): Promise<number> {
87
- const entries = await fs.readdir(dir, { withFileTypes: true });
93
+ let entries: Dirent[];
94
+ try {
95
+ entries = await fs.readdir(dir, { withFileTypes: true });
96
+ } catch (err) {
97
+ // Tolerate missing source trees (e.g. installs without the dashboard
98
+ // sources); the caller falls back to prebuilt assets or a clear build
99
+ // failure instead of crashing on the scan.
100
+ if (isEnoent(err)) return 0;
101
+ throw err;
102
+ }
88
103
 
89
104
  const promises = [];
90
105
  for (const entry of entries) {
@@ -108,7 +123,7 @@ async function getLatestMtime(dir: string): Promise<number> {
108
123
  }
109
124
 
110
125
  const ensureClientBuild = async () => {
111
- if (IS_BUN_COMPILED) return;
126
+ if (USE_EMBEDDED_CLIENT) return;
112
127
  const indexPath = path.join(STATIC_DIR, "index.html");
113
128
  const cssPath = path.join(STATIC_DIR, "styles.css");
114
129
  const clientSourceMtime = await getLatestMtime(CLIENT_DIR);
@@ -247,7 +262,7 @@ async function handleApi(req: Request): Promise<Response> {
247
262
  * Handle static file requests.
248
263
  */
249
264
  async function handleStatic(requestPath: string): Promise<Response> {
250
- const staticDir = IS_BUN_COMPILED ? await getCompiledClientDir() : STATIC_DIR;
265
+ const staticDir = await getEmbeddedClientDir();
251
266
  const filePath = requestPath === "/" ? "/index.html" : requestPath;
252
267
  const fullPath = path.join(staticDir, filePath);
253
268