@caius_kong/ccusage-dashboard 0.2.11 → 0.2.13
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 +7 -0
- package/bin/ccusage-ui.js +128 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,11 +50,18 @@ for you to open.
|
|
|
50
50
|
npx @caius_kong/ccusage-dashboard --port 9000 # change port
|
|
51
51
|
npx @caius_kong/ccusage-dashboard --budget 500 # monthly budget cap (default $300)
|
|
52
52
|
npx @caius_kong/ccusage-dashboard --no-warm # skip background warm-up
|
|
53
|
+
npx @caius_kong/ccusage-dashboard --foreground # run attached to this terminal (Ctrl+C stops it)
|
|
54
|
+
npx @caius_kong/ccusage-dashboard --stop # stop the background instance
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
Once started, the launcher prints the dashboard URL — open it in your browser
|
|
56
58
|
(no browser is auto-launched).
|
|
57
59
|
|
|
60
|
+
By default the dashboard runs **in the background**: after printing its URL the
|
|
61
|
+
launcher exits and the server keeps running, so you can close the terminal. Use
|
|
62
|
+
`--stop` to shut it down; running it again reuses the already-running instance.
|
|
63
|
+
`--foreground` keeps it attached to the terminal instead (Ctrl+C to stop).
|
|
64
|
+
|
|
58
65
|
## What it shows
|
|
59
66
|
|
|
60
67
|
| | |
|
package/bin/ccusage-ui.js
CHANGED
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
* the local python3, forwarding any CLI args. The server itself shells out to
|
|
7
7
|
* ccusage for all numbers, so the dashboard always matches ccusage.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Modes:
|
|
10
|
+
* default — start the server detached in the background, print
|
|
11
|
+
* the URL, and exit immediately. The server keeps running
|
|
12
|
+
* after this terminal closes. If a server is already up on
|
|
13
|
+
* the port, it reuses it instead of starting a second one.
|
|
14
|
+
* --foreground — run the server attached to this terminal (Ctrl+C stops it).
|
|
15
|
+
*
|
|
16
|
+
* Stop a background instance with: npx @caius_kong/ccusage-dashboard --stop
|
|
11
17
|
*/
|
|
12
18
|
'use strict';
|
|
13
19
|
|
|
@@ -15,6 +21,7 @@ const { spawn, spawnSync } = require('node:child_process');
|
|
|
15
21
|
const path = require('node:path');
|
|
16
22
|
const fs = require('node:fs');
|
|
17
23
|
const http = require('node:http');
|
|
24
|
+
const os = require('node:os');
|
|
18
25
|
|
|
19
26
|
// Data files live in package/lib (installed layout) or the repo root (dev layout).
|
|
20
27
|
function resolveLibFile(name) {
|
|
@@ -45,7 +52,7 @@ if (!py) {
|
|
|
45
52
|
process.exit(1);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
//
|
|
55
|
+
// --- arg parsing -----------------------------------------------------------
|
|
49
56
|
const rawArgs = process.argv.slice(2);
|
|
50
57
|
function argValue(name, fallback) {
|
|
51
58
|
const i = rawArgs.indexOf(name);
|
|
@@ -54,23 +61,132 @@ function argValue(name, fallback) {
|
|
|
54
61
|
const host = argValue('--host', '127.0.0.1');
|
|
55
62
|
const port = parseInt(argValue('--port', '8799'), 10);
|
|
56
63
|
const url = `http://${host}:${port}`;
|
|
64
|
+
const wantDaemon = !rawArgs.includes('--foreground'); // background by default
|
|
65
|
+
const wantStop = rawArgs.includes('--stop');
|
|
66
|
+
const wantForeground = rawArgs.includes('--foreground');
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
// Filter launcher-only flags before forwarding to server.py.
|
|
69
|
+
const serverArgs = rawArgs.filter((a) => !['--daemon', '--stop', '--foreground'].includes(a));
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
// --- daemon pid file (per host:port) ----------------------------------------
|
|
72
|
+
function pidFile() {
|
|
73
|
+
const safeHost = host.replace(/[^a-z0-9]/gi, '_');
|
|
74
|
+
return path.join(os.tmpdir(), `ccusage-dashboard-${safeHost}-${port}.pid`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readDaemonPid() {
|
|
78
|
+
try {
|
|
79
|
+
const pid = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
|
|
80
|
+
return Number.isFinite(pid) ? pid : null;
|
|
81
|
+
} catch { return null; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function daemonAlive(pid) {
|
|
85
|
+
if (!pid) return false;
|
|
86
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isServerUp() {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const req = http.get(url + '/api/health', { timeout: 1200 }, (res) => {
|
|
92
|
+
resolve(res.statusCode === 200);
|
|
93
|
+
});
|
|
94
|
+
req.on('error', () => resolve(false));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
61
97
|
|
|
62
|
-
// Once the server answers /, print a clear URL banner for manual opening.
|
|
63
|
-
let bannerPrinted = false;
|
|
64
98
|
function printBanner() {
|
|
65
|
-
if (bannerPrinted) return;
|
|
66
|
-
bannerPrinted = true;
|
|
67
99
|
const line = '─'.repeat(Math.max(20, url.length + 6));
|
|
68
100
|
console.log('');
|
|
69
101
|
console.log(line);
|
|
70
102
|
console.log(` Dashboard is running → ${url}`);
|
|
71
103
|
console.log(` Open it manually: ${url}`);
|
|
72
104
|
console.log(line);
|
|
73
|
-
|
|
105
|
+
if (wantDaemon) {
|
|
106
|
+
console.log(` (background — stop with: npx @caius_kong/ccusage-dashboard --stop)`);
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function waitForUp(timeoutMs) {
|
|
112
|
+
const start = Date.now();
|
|
113
|
+
while (Date.now() - start < timeoutMs) {
|
|
114
|
+
if (await isServerUp()) return true;
|
|
115
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- stop -------------------------------------------------------------------
|
|
121
|
+
async function handleStop() {
|
|
122
|
+
const pid = readDaemonPid();
|
|
123
|
+
if (pid && daemonAlive(pid)) {
|
|
124
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
125
|
+
console.log(`Stopping dashboard (pid ${pid}) …`);
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
while (Date.now() - t0 < 5000 && await isServerUp()) {
|
|
128
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
129
|
+
}
|
|
130
|
+
try { fs.unlinkSync(pidFile()); } catch {}
|
|
131
|
+
console.log('Dashboard stopped.');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
console.log('No running dashboard instance found.');
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (wantStop) {
|
|
139
|
+
handleStop();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- daemon mode ------------------------------------------------------------
|
|
144
|
+
async function handleDaemon() {
|
|
145
|
+
// Reuse an already-running instance on this port.
|
|
146
|
+
if (await isServerUp()) {
|
|
147
|
+
console.log(`A dashboard is already running at ${url} — reusing it.`);
|
|
148
|
+
printBanner();
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
// If the pid file points at a dead pid, clear it.
|
|
152
|
+
const oldPid = readDaemonPid();
|
|
153
|
+
if (oldPid && !daemonAlive(oldPid)) {
|
|
154
|
+
try { fs.unlinkSync(pidFile()); } catch {}
|
|
155
|
+
}
|
|
156
|
+
// Start detached; the server outlives this launcher.
|
|
157
|
+
const out = fs.openSync(path.join(os.tmpdir(), `ccusage-dashboard-${port}.log`), 'a');
|
|
158
|
+
const child = spawn(py, [serverPy, ...serverArgs], {
|
|
159
|
+
detached: true,
|
|
160
|
+
stdio: ['ignore', out, out],
|
|
161
|
+
env: { ...process.env },
|
|
162
|
+
});
|
|
163
|
+
child.unref();
|
|
164
|
+
fs.writeFileSync(pidFile(), String(child.pid));
|
|
165
|
+
const up = await waitForUp(15000);
|
|
166
|
+
if (!up) {
|
|
167
|
+
console.error(`[ccusage-dashboard] server did not come up within 15s — check ${path.join(os.tmpdir(), `ccusage-dashboard-${port}.log`)}`);
|
|
168
|
+
try { process.kill(child.pid, 'SIGTERM'); } catch {}
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
console.log(`ccusage-dashboard started in background (pid ${child.pid}).`);
|
|
172
|
+
printBanner();
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (wantDaemon) {
|
|
177
|
+
handleDaemon();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- foreground mode (default) ----------------------------------------------
|
|
182
|
+
const child = spawn(py, [serverPy, ...serverArgs], { stdio: 'inherit', env: { ...process.env } });
|
|
183
|
+
|
|
184
|
+
// Once the server answers /, print a clear URL banner for manual opening.
|
|
185
|
+
let bannerPrinted = false;
|
|
186
|
+
function printBannerFg() {
|
|
187
|
+
if (bannerPrinted) return;
|
|
188
|
+
bannerPrinted = true;
|
|
189
|
+
printBanner();
|
|
74
190
|
}
|
|
75
191
|
|
|
76
192
|
let checked = false;
|
|
@@ -78,7 +194,7 @@ function pingAndBanner() {
|
|
|
78
194
|
if (checked) return;
|
|
79
195
|
checked = true;
|
|
80
196
|
const req = http.get(url + '/api/health', { timeout: 1500 }, (res) => {
|
|
81
|
-
if (res.statusCode === 200)
|
|
197
|
+
if (res.statusCode === 200) printBannerFg();
|
|
82
198
|
else setTimeout(pingAndBanner, 500);
|
|
83
199
|
});
|
|
84
200
|
req.on('error', () => setTimeout(pingAndBanner, 500));
|
|
@@ -89,4 +205,4 @@ setTimeout(pingAndBanner, 800); // give the python server a beat to bind
|
|
|
89
205
|
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
90
206
|
process.on(sig, () => { if (!child.killed) child.kill(sig); });
|
|
91
207
|
}
|
|
92
|
-
child.on('exit', (code) => process.exit(code == null ? 0 : code));
|
|
208
|
+
child.on('exit', (code) => process.exit(code == null ? 0 : code));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caius_kong/ccusage-dashboard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "One-command local dashboard for ccusage: today/week/month/custom cost by model, 30-day trend, monthly budget alert. Numbers straight from ccusage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|