@oh-my-pi/omp-stats 15.10.12 → 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.
- package/dist/types/embedded-client.d.ts +18 -0
- package/package.json +4 -4
- package/src/embedded-client.generated.txt +0 -7
- package/src/embedded-client.ts +26 -0
- package/src/server.ts +37 -22
|
@@ -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.
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "15.
|
|
42
|
-
"@oh-my-pi/pi-utils": "15.
|
|
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
|
|
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
|
|
34
|
-
let
|
|
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
|
|
60
|
-
if (!
|
|
61
|
-
if (
|
|
64
|
+
async function getEmbeddedClientDir(): Promise<string> {
|
|
65
|
+
if (!USE_EMBEDDED_CLIENT) return STATIC_DIR;
|
|
66
|
+
if (embeddedClientDirPromise) return embeddedClientDirPromise;
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
const bundleHash = Bun.hash(
|
|
70
|
-
const outputDir = path.join(
|
|
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(
|
|
85
|
+
await extractEmbeddedClientArchive(EMBEDDED_CLIENT_ARCHIVE, outputDir);
|
|
80
86
|
return outputDir;
|
|
81
87
|
})();
|
|
82
88
|
|
|
83
|
-
return
|
|
89
|
+
return embeddedClientDirPromise;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
async function getLatestMtime(dir: string): Promise<number> {
|
|
87
|
-
|
|
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 (
|
|
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 =
|
|
265
|
+
const staticDir = await getEmbeddedClientDir();
|
|
251
266
|
const filePath = requestPath === "/" ? "/index.html" : requestPath;
|
|
252
267
|
const fullPath = path.join(staticDir, filePath);
|
|
253
268
|
|