@caius_kong/ccusage-dashboard 0.2.9 → 0.2.11

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 CHANGED
@@ -38,7 +38,8 @@ npx @caius_kong/ccusage-dashboard
38
38
  ```
39
39
 
40
40
  That's it. npx downloads the package (including its own `ccusage` dependency),
41
- starts a local server on `http://127.0.0.1:8799`, and opens your browser.
41
+ starts a local server on `http://127.0.0.1:8799`, and prints the dashboard URL
42
+ for you to open.
42
43
 
43
44
  > Requirements: **Node.js** (for the launcher) and **Python 3.8+** (for the server).
44
45
  > On macOS: `brew install python3`. No other installs, no build step, no config.
@@ -48,10 +49,12 @@ starts a local server on `http://127.0.0.1:8799`, and opens your browser.
48
49
  ```bash
49
50
  npx @caius_kong/ccusage-dashboard --port 9000 # change port
50
51
  npx @caius_kong/ccusage-dashboard --budget 500 # monthly budget cap (default $300)
51
- npx @caius_kong/ccusage-dashboard --no-warm # skip 3s startup warm-up
52
- CCUSAGE_UI_NO_OPEN=1 npx @caius_kong/ccusage-dashboard # don't auto-open browser
52
+ npx @caius_kong/ccusage-dashboard --no-warm # skip background warm-up
53
53
  ```
54
54
 
55
+ Once started, the launcher prints the dashboard URL — open it in your browser
56
+ (no browser is auto-launched).
57
+
55
58
  ## What it shows
56
59
 
57
60
  | | |
@@ -80,7 +83,7 @@ ccusage (bundled dependency — the real cost engine)
80
83
  - `lib/server.py` — Python stdlib HTTP server. Resolves a local ccusage
81
84
  (bundled dep → PATH → npx cache), warms caches on boot (~3s), then serves instant responses.
82
85
  - `lib/index.html` — single-file dashboard. No build step, no CDN.
83
- - `bin/ccusage-ui.js` — Node launcher (finds python3, starts server, opens browser).
86
+ - `bin/ccusage-ui.js` — Node launcher (finds python3, starts server, prints URL).
84
87
 
85
88
  ## Local development
86
89
 
package/bin/ccusage-ui.js CHANGED
@@ -5,12 +5,16 @@
5
5
  * Locates the bundled server.py (and its index.html sibling) and runs it with
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
+ *
9
+ * Startup UX: once the server is up, a clear URL banner is printed so the
10
+ * user can click/copy it to open the dashboard. No background magic.
8
11
  */
9
12
  'use strict';
10
13
 
11
14
  const { spawn, spawnSync } = require('node:child_process');
12
15
  const path = require('node:path');
13
16
  const fs = require('node:fs');
17
+ const http = require('node:http');
14
18
 
15
19
  // Data files live in package/lib (installed layout) or the repo root (dev layout).
16
20
  function resolveLibFile(name) {
@@ -41,13 +45,47 @@ if (!py) {
41
45
  process.exit(1);
42
46
  }
43
47
 
44
- const args = process.argv.slice(2);
45
- if (!args.includes('--open') && !process.env.CCUSAGE_UI_NO_OPEN) {
46
- args.push('--open'); // default: open browser after start
48
+ // Parse the user's --port / --host so the banner points at the real URL.
49
+ const rawArgs = process.argv.slice(2);
50
+ function argValue(name, fallback) {
51
+ const i = rawArgs.indexOf(name);
52
+ return i >= 0 && rawArgs[i + 1] ? rawArgs[i + 1] : fallback;
47
53
  }
54
+ const host = argValue('--host', '127.0.0.1');
55
+ const port = parseInt(argValue('--port', '8799'), 10);
56
+ const url = `http://${host}:${port}`;
57
+
58
+ const args = rawArgs.slice();
48
59
 
49
60
  const child = spawn(py, [serverPy, ...args], { stdio: 'inherit', env: { ...process.env } });
50
61
 
62
+ // Once the server answers /, print a clear URL banner for manual opening.
63
+ let bannerPrinted = false;
64
+ function printBanner() {
65
+ if (bannerPrinted) return;
66
+ bannerPrinted = true;
67
+ const line = '─'.repeat(Math.max(20, url.length + 6));
68
+ console.log('');
69
+ console.log(line);
70
+ console.log(` Dashboard is running → ${url}`);
71
+ console.log(` Open it manually: ${url}`);
72
+ console.log(line);
73
+ console.log('');
74
+ }
75
+
76
+ let checked = false;
77
+ function pingAndBanner() {
78
+ if (checked) return;
79
+ checked = true;
80
+ const req = http.get(url + '/api/health', { timeout: 1500 }, (res) => {
81
+ if (res.statusCode === 200) printBanner();
82
+ else setTimeout(pingAndBanner, 500);
83
+ });
84
+ req.on('error', () => setTimeout(pingAndBanner, 500));
85
+ }
86
+
87
+ setTimeout(pingAndBanner, 800); // give the python server a beat to bind
88
+
51
89
  for (const sig of ['SIGINT', 'SIGTERM']) {
52
90
  process.on(sig, () => { if (!child.killed) child.kill(sig); });
53
91
  }
package/lib/server.py CHANGED
@@ -12,7 +12,7 @@ Because all numbers come from ccusage itself, the figures always match what
12
12
  to maintain, no third-party packages — only the Python standard library.
13
13
 
14
14
  Usage:
15
- python3 server.py [--port 8799] [--budget 300] [--open]
15
+ python3 server.py [--port 8799] [--budget 300]
16
16
  """
17
17
  from __future__ import annotations
18
18
 
@@ -543,7 +543,6 @@ def main() -> None:
543
543
  parser.add_argument("--budget", type=float, default=None, help="monthly budget cap in USD (default 300)")
544
544
  parser.add_argument("--ccusage-path", default=None, help="explicit path to a ccusage binary or src/cli.js")
545
545
  parser.add_argument("--no-warm", action="store_true", help="skip background warm-up (first requests may be slow)")
546
- parser.add_argument("--open", action="store_true", help="open browser after start")
547
546
  args = parser.parse_args()
548
547
 
549
548
  BUDGET = args.budget if args.budget is not None else float(os_env_budget() or 300.0)
@@ -575,8 +574,6 @@ def main() -> None:
575
574
  httpd = ThreadingHTTPServer((args.host, args.port), Handler)
576
575
  url = f"http://{args.host}:{args.port}"
577
576
  print(f"ccusage-ui → {url} (monthly budget ${BUDGET:g}, Ctrl+C to stop)", flush=True)
578
- if args.open:
579
- _open_browser(url)
580
577
 
581
578
  try:
582
579
  httpd.serve_forever()
@@ -584,31 +581,6 @@ def main() -> None:
584
581
  print("\nbye", flush=True)
585
582
 
586
583
 
587
- def _open_browser(url: str) -> None:
588
- """Open the dashboard in the default browser. Prefers the OS-native opener
589
- (open / xdg-open / start) and logs success/failure so it is verifiable.
590
- """
591
- import subprocess
592
- import sys
593
-
594
- if sys.platform == "darwin":
595
- cmd = ["open", url]
596
- elif sys.platform.startswith("win"):
597
- cmd = ["cmd", "/c", "start", "", url]
598
- else:
599
- cmd = ["xdg-open", url]
600
- try:
601
- r = subprocess.run(cmd, timeout=5, capture_output=True, text=True)
602
- if r.returncode == 0:
603
- print(f"browser opened → {url}", flush=True)
604
- else:
605
- print(f"browser open failed (rc={r.returncode}): {r.stderr.strip()[:200]}", flush=True)
606
- except FileNotFoundError:
607
- print(f"no system opener found; open {url} manually", flush=True)
608
- except Exception as e: # noqa: BLE001
609
- print(f"browser open error: {e}", flush=True)
610
-
611
-
612
584
  def os_env_budget() -> str:
613
585
  import os
614
586
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caius_kong/ccusage-dashboard",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",