@mayurpise/wirespeed 1.0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/bin/wirespeed.js +2 -0
  4. package/dist/src/config.d.ts +19 -0
  5. package/dist/src/config.js +25 -0
  6. package/dist/src/config.js.map +1 -0
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +71 -0
  9. package/dist/src/index.js.map +1 -0
  10. package/dist/src/meta.d.ts +2 -0
  11. package/dist/src/meta.js +69 -0
  12. package/dist/src/meta.js.map +1 -0
  13. package/dist/src/orchestrator.d.ts +7 -0
  14. package/dist/src/orchestrator.js +38 -0
  15. package/dist/src/orchestrator.js.map +1 -0
  16. package/dist/src/stats/calculator.d.ts +6 -0
  17. package/dist/src/stats/calculator.js +41 -0
  18. package/dist/src/stats/calculator.js.map +1 -0
  19. package/dist/src/stats/units.d.ts +4 -0
  20. package/dist/src/stats/units.js +25 -0
  21. package/dist/src/stats/units.js.map +1 -0
  22. package/dist/src/tester/download.d.ts +2 -0
  23. package/dist/src/tester/download.js +59 -0
  24. package/dist/src/tester/download.js.map +1 -0
  25. package/dist/src/tester/http-client.d.ts +3 -0
  26. package/dist/src/tester/http-client.js +72 -0
  27. package/dist/src/tester/http-client.js.map +1 -0
  28. package/dist/src/tester/latency.d.ts +2 -0
  29. package/dist/src/tester/latency.js +25 -0
  30. package/dist/src/tester/latency.js.map +1 -0
  31. package/dist/src/tester/types.d.ts +44 -0
  32. package/dist/src/tester/types.js +2 -0
  33. package/dist/src/tester/types.js.map +1 -0
  34. package/dist/src/tester/upload.d.ts +2 -0
  35. package/dist/src/tester/upload.js +61 -0
  36. package/dist/src/tester/upload.js.map +1 -0
  37. package/dist/src/ui/components.d.ts +4 -0
  38. package/dist/src/ui/components.js +39 -0
  39. package/dist/src/ui/components.js.map +1 -0
  40. package/dist/src/ui/layout.d.ts +4 -0
  41. package/dist/src/ui/layout.js +111 -0
  42. package/dist/src/ui/layout.js.map +1 -0
  43. package/dist/src/ui/renderer.d.ts +9 -0
  44. package/dist/src/ui/renderer.js +36 -0
  45. package/dist/src/ui/renderer.js.map +1 -0
  46. package/dist/src/ui/theme.d.ts +33 -0
  47. package/dist/src/ui/theme.js +43 -0
  48. package/dist/src/ui/theme.js.map +1 -0
  49. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mayurpise
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # wirespeed
2
+
3
+ Fast, zero-dependency internet speed test for the terminal. Measures download, upload, and latency using Cloudflare's global edge network.
4
+
5
+ ```
6
+ $ npx wirespeed
7
+
8
+ wirespeed v1.0.0
9
+
10
+ Server Cloudflare — Mumbai (BOM)
11
+ IP 203.0.113.42
12
+
13
+ Latency 12.34 ms (jitter: 1.23 ms)
14
+ Download 245.67 Mbps
15
+ Upload 98.12 Mbps
16
+ ```
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install -g @mayurpise/wirespeed
22
+ ```
23
+
24
+ Or run directly:
25
+
26
+ ```bash
27
+ npx @mayurpise/wirespeed
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ wirespeed # Run full speed test
34
+ wirespeed --json # Output results as JSON
35
+ wirespeed --no-upload # Skip upload test
36
+ wirespeed --no-download # Skip download test
37
+ ```
38
+
39
+ ### Options
40
+
41
+ | Flag | Description |
42
+ | ---------------- | -------------------- |
43
+ | `--json` | Output results as JSON |
44
+ | `--no-download` | Skip download test |
45
+ | `--no-upload` | Skip upload test |
46
+ | `-h`, `--help` | Show help |
47
+ | `-v`, `--version`| Show version |
48
+
49
+ ### JSON output
50
+
51
+ ```bash
52
+ wirespeed --json | jq .
53
+ ```
54
+
55
+ ```json
56
+ {
57
+ "server": { "colo": "BOM", "city": "Mumbai", "ip": "203.0.113.42" },
58
+ "latency": { "median": 12.34, "jitter": 1.23, "unit": "ms" },
59
+ "download": { "speed": 245.67, "unit": "Mbps" },
60
+ "upload": { "speed": 98.12, "unit": "Mbps" }
61
+ }
62
+ ```
63
+
64
+ ## How it works
65
+
66
+ wirespeed uses Cloudflare's speed test infrastructure (`speed.cloudflare.com`):
67
+
68
+ 1. **Server detection** — Identifies the nearest Cloudflare edge via `/cdn-cgi/trace`
69
+ 2. **Latency** — 20 zero-byte round trips, reports median and jitter
70
+ 3. **Download** — Multi-phase parallel requests with increasing payload sizes (100KB to 100MB)
71
+ 4. **Upload** — Multi-phase parallel uploads (100KB to 10MB)
72
+
73
+ Bandwidth is calculated at the 90th percentile across all measurements for accuracy. Real-time speed is smoothed with an exponential moving average.
74
+
75
+ ## Requirements
76
+
77
+ - Node.js >= 18.0.0
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/src/index.js';
@@ -0,0 +1,19 @@
1
+ export declare const CF_DOWN_URL = "https://speed.cloudflare.com/__down";
2
+ export declare const CF_UP_URL = "https://speed.cloudflare.com/__up";
3
+ export declare const CF_TRACE_URL = "https://speed.cloudflare.com/cdn-cgi/trace";
4
+ export declare const CF_LOCATIONS_URL = "https://speed.cloudflare.com/locations";
5
+ export declare const LATENCY_REQUESTS = 20;
6
+ export declare const BANDWIDTH_PERCENTILE = 0.9;
7
+ export declare const LATENCY_PERCENTILE = 0.5;
8
+ export declare const MIN_REQUEST_DURATION_MS = 10;
9
+ export declare const FINISH_REQUEST_DURATION_MS = 1000;
10
+ export declare const ESTIMATED_SERVER_TIME_MS = 10;
11
+ export declare const RENDER_INTERVAL_MS = 80;
12
+ export declare const EMA_ALPHA = 0.3;
13
+ export interface PhaseConfig {
14
+ bytes: number;
15
+ count: number;
16
+ parallel: number;
17
+ }
18
+ export declare const DOWNLOAD_PHASES: PhaseConfig[];
19
+ export declare const UPLOAD_PHASES: PhaseConfig[];
@@ -0,0 +1,25 @@
1
+ export const CF_DOWN_URL = 'https://speed.cloudflare.com/__down';
2
+ export const CF_UP_URL = 'https://speed.cloudflare.com/__up';
3
+ export const CF_TRACE_URL = 'https://speed.cloudflare.com/cdn-cgi/trace';
4
+ export const CF_LOCATIONS_URL = 'https://speed.cloudflare.com/locations';
5
+ export const LATENCY_REQUESTS = 20;
6
+ export const BANDWIDTH_PERCENTILE = 0.9;
7
+ export const LATENCY_PERCENTILE = 0.5;
8
+ export const MIN_REQUEST_DURATION_MS = 10;
9
+ export const FINISH_REQUEST_DURATION_MS = 1000;
10
+ export const ESTIMATED_SERVER_TIME_MS = 10;
11
+ export const RENDER_INTERVAL_MS = 80;
12
+ export const EMA_ALPHA = 0.3;
13
+ export const DOWNLOAD_PHASES = [
14
+ { bytes: 100_000, count: 2, parallel: 1 },
15
+ { bytes: 1_000_000, count: 4, parallel: 4 },
16
+ { bytes: 10_000_000, count: 3, parallel: 6 },
17
+ { bytes: 25_000_000, count: 2, parallel: 6 },
18
+ { bytes: 100_000_000, count: 1, parallel: 4 },
19
+ ];
20
+ export const UPLOAD_PHASES = [
21
+ { bytes: 100_000, count: 2, parallel: 1 },
22
+ { bytes: 1_000_000, count: 4, parallel: 3 },
23
+ { bytes: 10_000_000, count: 2, parallel: 3 },
24
+ ];
25
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,qCAAqC,CAAC;AACjE,MAAM,CAAC,MAAM,SAAS,GAAG,mCAAmC,CAAC;AAC7D,MAAM,CAAC,MAAM,YAAY,GAAG,4CAA4C,CAAC;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,wCAAwC,CAAC;AAEzE,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AACxC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAC1C,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAC/C,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAC3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAQ7B,MAAM,CAAC,MAAM,eAAe,GAAkB;IAC5C,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;IACzC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;IAC5C,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;IAC5C,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;CAC9C,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;IACzC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;CAC7C,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { Renderer } from './ui/renderer.js';
3
+ import { composePlainResult, composeJson } from './ui/layout.js';
4
+ import { runSpeedTest } from './orchestrator.js';
5
+ const { values } = parseArgs({
6
+ options: {
7
+ json: { type: 'boolean', default: false },
8
+ 'no-upload': { type: 'boolean', default: false },
9
+ 'no-download': { type: 'boolean', default: false },
10
+ help: { type: 'boolean', short: 'h', default: false },
11
+ version: { type: 'boolean', short: 'v', default: false },
12
+ },
13
+ strict: true,
14
+ });
15
+ if (values.help) {
16
+ console.log(`
17
+ wirespeed - Terminal internet speed test
18
+
19
+ Usage: wirespeed [options]
20
+
21
+ Options:
22
+ --json Output results as JSON
23
+ --no-download Skip download test
24
+ --no-upload Skip upload test
25
+ -h, --help Show this help
26
+ -v, --version Show version
27
+ `);
28
+ process.exit(0);
29
+ }
30
+ if (values.version) {
31
+ console.log('wirespeed v1.0.0');
32
+ process.exit(0);
33
+ }
34
+ const isInteractive = process.stdout.isTTY && !values.json;
35
+ const renderer = new Renderer();
36
+ if (isInteractive) {
37
+ renderer.start();
38
+ }
39
+ try {
40
+ const results = await runSpeedTest(renderer, {
41
+ noDownload: values['no-download'] ?? false,
42
+ noUpload: values['no-upload'] ?? false,
43
+ });
44
+ if (isInteractive) {
45
+ renderer.stop();
46
+ }
47
+ else {
48
+ const finalState = {
49
+ phase: 'done',
50
+ progress: 1,
51
+ currentSpeed: 0,
52
+ server: results.server,
53
+ latency: results.latency,
54
+ download: results.download ?? undefined,
55
+ upload: results.upload ?? undefined,
56
+ };
57
+ if (values.json) {
58
+ console.log(composeJson(finalState));
59
+ }
60
+ else {
61
+ console.log(composePlainResult(finalState));
62
+ }
63
+ }
64
+ }
65
+ catch (err) {
66
+ if (isInteractive)
67
+ renderer.stop();
68
+ console.error('Speed test failed:', err instanceof Error ? err.message : err);
69
+ process.exit(1);
70
+ }
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE;QACP,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QACzC,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QAChD,aAAa,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;QAClD,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;QACrD,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;KACzD;IACD,MAAM,EAAE,IAAI;CACb,CAAC,CAAC;AAEH,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;GAWX,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAChC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;AAE3D,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;AAEhC,IAAI,aAAa,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAED,IAAI,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,QAAQ,EAAE;QAC3C,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC,IAAI,KAAK;QAC1C,QAAQ,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,KAAK;KACvC,CAAC,CAAC;IAEH,IAAI,aAAa,EAAE,CAAC;QAClB,QAAQ,CAAC,IAAI,EAAE,CAAC;IAClB,CAAC;SAAM,CAAC;QACN,MAAM,UAAU,GAAe;YAC7B,KAAK,EAAE,MAAM;YACb,QAAQ,EAAE,CAAC;YACX,YAAY,EAAE,CAAC;YACf,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS;YACvC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,SAAS;SACpC,CAAC;QAEF,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;QACvC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;AACH,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,IAAI,aAAa;QAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC9E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ServerMeta } from './tester/types.js';
2
+ export declare function fetchServerMeta(): Promise<ServerMeta>;
@@ -0,0 +1,69 @@
1
+ import { CF_TRACE_URL, CF_LOCATIONS_URL } from './config.js';
2
+ // Fallback map for common Cloudflare datacenters when the locations API is unavailable
3
+ const COLO_CITIES = {
4
+ ATL: 'Atlanta', IAD: 'Ashburn', BOS: 'Boston', ORD: 'Chicago', DFW: 'Dallas',
5
+ DEN: 'Denver', DTW: 'Detroit', HNL: 'Honolulu', IAH: 'Houston', JAX: 'Jacksonville',
6
+ MCI: 'Kansas City', LAS: 'Las Vegas', LAX: 'Los Angeles', MIA: 'Miami',
7
+ MSP: 'Minneapolis', BNA: 'Nashville', EWR: 'Newark', JFK: 'New York',
8
+ PHL: 'Philadelphia', PHX: 'Phoenix', PDX: 'Portland', RIC: 'Richmond',
9
+ SMF: 'Sacramento', SLC: 'Salt Lake City', SAN: 'San Diego', SFO: 'San Francisco',
10
+ SJC: 'San Jose', SEA: 'Seattle', STL: 'St. Louis', TPA: 'Tampa',
11
+ AMS: 'Amsterdam', ARN: 'Stockholm', BCN: 'Barcelona', BER: 'Berlin',
12
+ BRU: 'Brussels', BUD: 'Budapest', CDG: 'Paris', CPH: 'Copenhagen',
13
+ DUB: 'Dublin', DUS: 'Düsseldorf', FRA: 'Frankfurt', HAM: 'Hamburg',
14
+ HEL: 'Helsinki', LHR: 'London', LIS: 'Lisbon', MAD: 'Madrid',
15
+ MAN: 'Manchester', MRS: 'Marseille', MXP: 'Milan', MUC: 'Munich',
16
+ OSL: 'Oslo', PRG: 'Prague', VIE: 'Vienna', WAW: 'Warsaw', ZRH: 'Zurich',
17
+ NRT: 'Tokyo', HND: 'Tokyo', KIX: 'Osaka', ICN: 'Seoul', HKG: 'Hong Kong',
18
+ SIN: 'Singapore', BOM: 'Mumbai', DEL: 'Delhi', MAA: 'Chennai', BLR: 'Bangalore',
19
+ HYD: 'Hyderabad', SYD: 'Sydney', MEL: 'Melbourne', AKL: 'Auckland',
20
+ GRU: 'São Paulo', GIG: 'Rio de Janeiro', SCL: 'Santiago', BOG: 'Bogotá',
21
+ MEX: 'Mexico City', YYZ: 'Toronto', YVR: 'Vancouver', YUL: 'Montreal',
22
+ JNB: 'Johannesburg', CPT: 'Cape Town', CAI: 'Cairo', DXB: 'Dubai',
23
+ DOH: 'Doha', RUH: 'Riyadh', TPE: 'Taipei', KUL: 'Kuala Lumpur',
24
+ BKK: 'Bangkok', CGK: 'Jakarta', MNL: 'Manila', PEK: 'Beijing', PVG: 'Shanghai',
25
+ CAN: 'Guangzhou',
26
+ };
27
+ export async function fetchServerMeta() {
28
+ const defaults = { colo: '???', city: 'Unknown', ip: '', loc: '' };
29
+ try {
30
+ const traceRes = await fetch(CF_TRACE_URL, { headers: { 'User-Agent': 'wirespeed/1.0.0' } });
31
+ if (!traceRes.ok)
32
+ return defaults;
33
+ const traceText = await traceRes.text();
34
+ const traceMap = new Map();
35
+ for (const line of traceText.split('\n')) {
36
+ const eq = line.indexOf('=');
37
+ if (eq > 0) {
38
+ traceMap.set(line.slice(0, eq), line.slice(eq + 1));
39
+ }
40
+ }
41
+ const colo = traceMap.get('colo') ?? defaults.colo;
42
+ const ip = traceMap.get('ip') ?? defaults.ip;
43
+ const loc = traceMap.get('loc') ?? defaults.loc;
44
+ // Try locations API first, fall back to built-in map
45
+ let city = COLO_CITIES[colo] ?? colo;
46
+ try {
47
+ const locationsRes = await fetch(CF_LOCATIONS_URL, {
48
+ headers: { 'User-Agent': 'wirespeed/1.0.0' },
49
+ signal: AbortSignal.timeout(3000),
50
+ });
51
+ if (locationsRes.ok) {
52
+ const data = await locationsRes.json();
53
+ if (Array.isArray(data)) {
54
+ const match = data.find(l => l.iata === colo);
55
+ if (match)
56
+ city = match.city;
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Use fallback map
62
+ }
63
+ return { colo, city, ip, loc };
64
+ }
65
+ catch {
66
+ return defaults;
67
+ }
68
+ }
69
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/meta.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { Renderer } from './ui/renderer.js';
2
+ import type { TestResults } from './tester/types.js';
3
+ export interface RunOptions {
4
+ noDownload: boolean;
5
+ noUpload: boolean;
6
+ }
7
+ export declare function runSpeedTest(renderer: Renderer, options: RunOptions): Promise<TestResults>;
@@ -0,0 +1,38 @@
1
+ import { fetchServerMeta } from './meta.js';
2
+ import { measureLatency } from './tester/latency.js';
3
+ import { measureDownload } from './tester/download.js';
4
+ import { measureUpload } from './tester/upload.js';
5
+ export async function runSpeedTest(renderer, options) {
6
+ // Phase: init
7
+ renderer.update({ phase: 'init', progress: 0, currentSpeed: 0 });
8
+ const server = await fetchServerMeta();
9
+ renderer.update({ server });
10
+ // Phase: latency
11
+ renderer.update({ phase: 'latency', progress: 0, currentSpeed: 0 });
12
+ const latency = await measureLatency((_i, _ms) => {
13
+ // Could update live latency display here if desired
14
+ });
15
+ renderer.update({ latency });
16
+ // Phase: download
17
+ let download = null;
18
+ if (!options.noDownload) {
19
+ renderer.update({ phase: 'download', progress: 0, currentSpeed: 0 });
20
+ download = await measureDownload((progress, currentSpeed) => {
21
+ renderer.update({ progress, currentSpeed });
22
+ });
23
+ renderer.update({ download, progress: 1 });
24
+ }
25
+ // Phase: upload
26
+ let upload = null;
27
+ if (!options.noUpload) {
28
+ renderer.update({ phase: 'upload', progress: 0, currentSpeed: 0 });
29
+ upload = await measureUpload((progress, currentSpeed) => {
30
+ renderer.update({ progress, currentSpeed });
31
+ });
32
+ renderer.update({ upload, progress: 1 });
33
+ }
34
+ // Done
35
+ renderer.update({ phase: 'done' });
36
+ return { server, latency, download, upload };
37
+ }
38
+ //# sourceMappingURL=orchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/orchestrator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AASnD,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAAkB,EAClB,OAAmB;IAEnB,cAAc;IACd,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAG,MAAM,eAAe,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAE5B,iBAAiB;IACjB,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE;QAC/C,oDAAoD;IACtD,CAAC,CAAC,CAAC;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IAE7B,kBAAkB;IAClB,IAAI,QAAQ,GAA4B,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;QACrE,QAAQ,GAAG,MAAM,eAAe,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE;YAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,gBAAgB;IAChB,IAAI,MAAM,GAA0B,IAAI,CAAC;IACzC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;QACnE,MAAM,GAAG,MAAM,aAAa,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE;YACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO;IACP,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAEnC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Measurement } from '../tester/types.js';
2
+ export declare function percentile(sorted: number[], p: number): number;
3
+ export declare function computeBandwidth(measurements: Measurement[]): number;
4
+ export declare function computeMedianLatency(latencies: number[]): number;
5
+ export declare function computeJitter(latencies: number[]): number;
6
+ export declare function ema(current: number, previous: number): number;
@@ -0,0 +1,41 @@
1
+ import { MIN_REQUEST_DURATION_MS, BANDWIDTH_PERCENTILE, LATENCY_PERCENTILE, EMA_ALPHA } from '../config.js';
2
+ export function percentile(sorted, p) {
3
+ if (sorted.length === 0)
4
+ return 0;
5
+ if (sorted.length === 1)
6
+ return sorted[0];
7
+ const pos = (sorted.length - 1) * p;
8
+ const base = Math.floor(pos);
9
+ const rest = pos - base;
10
+ const next = sorted[base + 1];
11
+ if (next !== undefined) {
12
+ return sorted[base] + rest * (next - sorted[base]);
13
+ }
14
+ return sorted[base];
15
+ }
16
+ export function computeBandwidth(measurements) {
17
+ const valid = measurements
18
+ .filter(m => m.durationMs >= MIN_REQUEST_DURATION_MS)
19
+ .map(m => m.speedBps)
20
+ .sort((a, b) => a - b);
21
+ if (valid.length === 0)
22
+ return 0;
23
+ return percentile(valid, BANDWIDTH_PERCENTILE);
24
+ }
25
+ export function computeMedianLatency(latencies) {
26
+ const sorted = [...latencies].sort((a, b) => a - b);
27
+ return percentile(sorted, LATENCY_PERCENTILE);
28
+ }
29
+ export function computeJitter(latencies) {
30
+ if (latencies.length < 2)
31
+ return 0;
32
+ let sum = 0;
33
+ for (let i = 1; i < latencies.length; i++) {
34
+ sum += Math.abs(latencies[i] - latencies[i - 1]);
35
+ }
36
+ return sum / (latencies.length - 1);
37
+ }
38
+ export function ema(current, previous) {
39
+ return EMA_ALPHA * current + (1 - EMA_ALPHA) * previous;
40
+ }
41
+ //# sourceMappingURL=calculator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculator.js","sourceRoot":"","sources":["../../../src/stats/calculator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE5G,MAAM,UAAU,UAAU,CAAC,MAAgB,EAAE,CAAS;IACpD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,CAAC,CAAE,CAAC;IAE3C,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC;IAExB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAC9B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC,IAAI,CAAE,GAAG,IAAI,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAE,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,YAA2B;IAC1D,MAAM,KAAK,GAAG,YAAY;SACvB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,uBAAuB,CAAC;SACpD,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;SACpB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAEzB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACjC,OAAO,UAAU,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,SAAmB;IACtD,MAAM,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,OAAO,UAAU,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAmB;IAC/C,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAE,GAAG,SAAS,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,QAAgB;IACnD,OAAO,SAAS,GAAG,OAAO,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,QAAQ,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function formatSpeed(bps: number): string;
2
+ export declare function formatLatency(ms: number): string;
3
+ export declare function formatBytes(bytes: number): string;
4
+ export declare function bpsToMbps(bps: number): number;
@@ -0,0 +1,25 @@
1
+ export function formatSpeed(bps) {
2
+ const mbps = bps / 1_000_000;
3
+ if (mbps >= 1000) {
4
+ return `${(mbps / 1000).toFixed(2)} Gbps`;
5
+ }
6
+ return `${mbps.toFixed(2)} Mbps`;
7
+ }
8
+ export function formatLatency(ms) {
9
+ if (ms < 1)
10
+ return `${(ms * 1000).toFixed(0)} µs`;
11
+ return `${ms.toFixed(1)} ms`;
12
+ }
13
+ export function formatBytes(bytes) {
14
+ if (bytes >= 1_000_000_000)
15
+ return `${(bytes / 1_000_000_000).toFixed(1)} GB`;
16
+ if (bytes >= 1_000_000)
17
+ return `${(bytes / 1_000_000).toFixed(1)} MB`;
18
+ if (bytes >= 1_000)
19
+ return `${(bytes / 1_000).toFixed(1)} KB`;
20
+ return `${bytes} B`;
21
+ }
22
+ export function bpsToMbps(bps) {
23
+ return bps / 1_000_000;
24
+ }
25
+ //# sourceMappingURL=units.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"units.js","sourceRoot":"","sources":["../../../src/stats/units.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,IAAI,GAAG,GAAG,GAAG,SAAS,CAAC;IAC7B,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QACjB,OAAO,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAC5C,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAU;IACtC,IAAI,EAAE,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAClD,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,IAAI,aAAa;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAC9E,IAAI,KAAK,IAAI,SAAS;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IACtE,IAAI,KAAK,IAAI,KAAK;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAC9D,OAAO,GAAG,KAAK,IAAI,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,OAAO,GAAG,GAAG,SAAS,CAAC;AACzB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { BandwidthResult } from './types.js';
2
+ export declare function measureDownload(onProgress?: (progress: number, currentSpeedBps: number) => void): Promise<BandwidthResult>;
@@ -0,0 +1,59 @@
1
+ import { CF_DOWN_URL, DOWNLOAD_PHASES, FINISH_REQUEST_DURATION_MS } from '../config.js';
2
+ import { timedDownload } from './http-client.js';
3
+ import { computeBandwidth, ema } from '../stats/calculator.js';
4
+ import { bpsToMbps } from '../stats/units.js';
5
+ export async function measureDownload(onProgress) {
6
+ const allMeasurements = [];
7
+ let liveSpeed = 0;
8
+ let totalRequests = 0;
9
+ let completedRequests = 0;
10
+ // Count total requests for progress
11
+ for (const phase of DOWNLOAD_PHASES) {
12
+ totalRequests += phase.count;
13
+ }
14
+ for (const phase of DOWNLOAD_PHASES) {
15
+ const url = `${CF_DOWN_URL}?bytes=${phase.bytes}`;
16
+ let remaining = phase.count;
17
+ let phaseHitThreshold = false;
18
+ while (remaining > 0) {
19
+ const batch = Math.min(remaining, phase.parallel);
20
+ const promises = Array.from({ length: batch }, () => timedDownload(url).then(timing => {
21
+ const durationMs = timing.endTime - timing.ttfb;
22
+ const speedBps = durationMs > 0
23
+ ? (timing.bytesTransferred * 8 * 1000) / durationMs
24
+ : 0;
25
+ const m = {
26
+ bytes: timing.bytesTransferred,
27
+ durationMs,
28
+ speedBps,
29
+ };
30
+ allMeasurements.push(m);
31
+ if (speedBps > 0) {
32
+ liveSpeed = liveSpeed === 0 ? speedBps : ema(speedBps, liveSpeed);
33
+ }
34
+ if (durationMs > FINISH_REQUEST_DURATION_MS) {
35
+ phaseHitThreshold = true;
36
+ }
37
+ completedRequests++;
38
+ onProgress?.(completedRequests / totalRequests, liveSpeed);
39
+ return m;
40
+ }).catch(() => {
41
+ completedRequests++;
42
+ onProgress?.(completedRequests / totalRequests, liveSpeed);
43
+ return null;
44
+ }));
45
+ await Promise.all(promises);
46
+ remaining -= batch;
47
+ }
48
+ // Early termination: if requests in this phase took long enough, skip larger phases
49
+ if (phaseHitThreshold)
50
+ break;
51
+ }
52
+ const speedBps = computeBandwidth(allMeasurements);
53
+ return {
54
+ measurements: allMeasurements,
55
+ speedBps,
56
+ speedMbps: bpsToMbps(speedBps),
57
+ };
58
+ }
59
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../../../src/tester/download.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { TimingResult } from './types.js';
2
+ export declare function timedDownload(url: string): Promise<TimingResult>;
3
+ export declare function timedUpload(url: string, payload: Uint8Array): Promise<TimingResult>;
@@ -0,0 +1,72 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { ESTIMATED_SERVER_TIME_MS } from '../config.js';
3
+ function parseServerTiming(headers) {
4
+ const st = headers.get('server-timing');
5
+ if (st) {
6
+ const match = /dur=([\d.]+)/.exec(st);
7
+ if (match?.[1])
8
+ return parseFloat(match[1]);
9
+ }
10
+ return ESTIMATED_SERVER_TIME_MS;
11
+ }
12
+ export async function timedDownload(url) {
13
+ const startTime = performance.now();
14
+ const response = await fetch(url, {
15
+ cache: 'no-store',
16
+ headers: { 'User-Agent': 'wirespeed/1.0.0' },
17
+ });
18
+ if (!response.ok)
19
+ throw new Error(`HTTP ${response.status}`);
20
+ if (!response.body)
21
+ throw new Error('No response body');
22
+ let ttfb = 0;
23
+ let bytesTransferred = 0;
24
+ let firstChunk = true;
25
+ const reader = response.body.getReader();
26
+ for (;;) {
27
+ const { done, value } = await reader.read();
28
+ if (firstChunk) {
29
+ ttfb = performance.now();
30
+ firstChunk = false;
31
+ }
32
+ if (done)
33
+ break;
34
+ bytesTransferred += value.byteLength;
35
+ }
36
+ // If no body bytes (zero-byte latency test), ttfb is the end time
37
+ if (firstChunk)
38
+ ttfb = performance.now();
39
+ const endTime = performance.now();
40
+ const serverTime = parseServerTiming(response.headers);
41
+ return { startTime, ttfb, endTime, bytesTransferred, serverTime };
42
+ }
43
+ export async function timedUpload(url, payload) {
44
+ const startTime = performance.now();
45
+ const response = await fetch(url, {
46
+ method: 'POST',
47
+ body: Buffer.from(payload),
48
+ cache: 'no-store',
49
+ headers: {
50
+ 'User-Agent': 'wirespeed/1.0.0',
51
+ 'Content-Type': 'application/octet-stream',
52
+ },
53
+ });
54
+ if (!response.ok)
55
+ throw new Error(`HTTP ${response.status}`);
56
+ // Consume response body
57
+ if (response.body) {
58
+ const reader = response.body.getReader();
59
+ while (!(await reader.read()).done) { }
60
+ }
61
+ const endTime = performance.now();
62
+ const ttfb = endTime; // not meaningful for upload
63
+ const serverTime = parseServerTiming(response.headers);
64
+ return {
65
+ startTime,
66
+ ttfb,
67
+ endTime,
68
+ bytesTransferred: payload.byteLength,
69
+ serverTime,
70
+ };
71
+ }
72
+ //# sourceMappingURL=http-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.js","sourceRoot":"","sources":["../../../src/tester/http-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { LatencyResult } from './types.js';
2
+ export declare function measureLatency(onMeasurement?: (index: number, latencyMs: number) => void): Promise<LatencyResult>;
@@ -0,0 +1,25 @@
1
+ import { CF_DOWN_URL, LATENCY_REQUESTS } from '../config.js';
2
+ import { timedDownload } from './http-client.js';
3
+ import { computeMedianLatency, computeJitter } from '../stats/calculator.js';
4
+ export async function measureLatency(onMeasurement) {
5
+ const url = `${CF_DOWN_URL}?bytes=0`;
6
+ const measurements = [];
7
+ for (let i = 0; i < LATENCY_REQUESTS; i++) {
8
+ try {
9
+ const timing = await timedDownload(url);
10
+ const latencyMs = (timing.ttfb - timing.startTime) - timing.serverTime;
11
+ const clamped = Math.max(0, latencyMs);
12
+ measurements.push(clamped);
13
+ onMeasurement?.(i, clamped);
14
+ }
15
+ catch {
16
+ // Skip failed measurements
17
+ }
18
+ }
19
+ return {
20
+ measurements,
21
+ median: computeMedianLatency(measurements),
22
+ jitter: computeJitter(measurements),
23
+ };
24
+ }
25
+ //# sourceMappingURL=latency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"latency.js","sourceRoot":"","sources":["../../../src/tester/latency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG7E,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,aAA0D;IAE1D,MAAM,GAAG,GAAG,GAAG,WAAW,UAAU,CAAC;IACrC,MAAM,YAAY,GAAa,EAAE,CAAC;IAElC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,gBAAgB,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;YACxC,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC;YACvE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;YACvC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3B,aAAa,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,2BAA2B;QAC7B,CAAC;IACH,CAAC;IAED,OAAO;QACL,YAAY;QACZ,MAAM,EAAE,oBAAoB,CAAC,YAAY,CAAC;QAC1C,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,44 @@
1
+ export interface TimingResult {
2
+ startTime: number;
3
+ ttfb: number;
4
+ endTime: number;
5
+ bytesTransferred: number;
6
+ serverTime: number;
7
+ }
8
+ export interface Measurement {
9
+ bytes: number;
10
+ durationMs: number;
11
+ speedBps: number;
12
+ }
13
+ export interface LatencyResult {
14
+ measurements: number[];
15
+ median: number;
16
+ jitter: number;
17
+ }
18
+ export interface BandwidthResult {
19
+ measurements: Measurement[];
20
+ speedBps: number;
21
+ speedMbps: number;
22
+ }
23
+ export interface ServerMeta {
24
+ colo: string;
25
+ city: string;
26
+ ip: string;
27
+ loc: string;
28
+ }
29
+ export interface TestResults {
30
+ server: ServerMeta;
31
+ latency: LatencyResult;
32
+ download: BandwidthResult | null;
33
+ upload: BandwidthResult | null;
34
+ }
35
+ export type TestPhase = 'init' | 'latency' | 'download' | 'upload' | 'done';
36
+ export interface LiveUpdate {
37
+ phase: TestPhase;
38
+ progress: number;
39
+ currentSpeed: number;
40
+ latency?: LatencyResult;
41
+ download?: BandwidthResult;
42
+ upload?: BandwidthResult;
43
+ server?: ServerMeta;
44
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/tester/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ import type { BandwidthResult } from './types.js';
2
+ export declare function measureUpload(onProgress?: (progress: number, currentSpeedBps: number) => void): Promise<BandwidthResult>;
@@ -0,0 +1,61 @@
1
+ import { CF_UP_URL, UPLOAD_PHASES, FINISH_REQUEST_DURATION_MS } from '../config.js';
2
+ import { timedUpload } from './http-client.js';
3
+ import { computeBandwidth, ema } from '../stats/calculator.js';
4
+ import { bpsToMbps } from '../stats/units.js';
5
+ function generatePayload(bytes) {
6
+ // Fill with zeros — Cloudflare discards the body anyway
7
+ return new Uint8Array(bytes);
8
+ }
9
+ export async function measureUpload(onProgress) {
10
+ const allMeasurements = [];
11
+ let liveSpeed = 0;
12
+ let totalRequests = 0;
13
+ let completedRequests = 0;
14
+ for (const phase of UPLOAD_PHASES) {
15
+ totalRequests += phase.count;
16
+ }
17
+ for (const phase of UPLOAD_PHASES) {
18
+ const payload = generatePayload(phase.bytes);
19
+ let remaining = phase.count;
20
+ let phaseHitThreshold = false;
21
+ while (remaining > 0) {
22
+ const batch = Math.min(remaining, phase.parallel);
23
+ const promises = Array.from({ length: batch }, () => timedUpload(CF_UP_URL, payload).then(timing => {
24
+ const durationMs = (timing.endTime - timing.startTime) - timing.serverTime;
25
+ const speedBps = durationMs > 0
26
+ ? (timing.bytesTransferred * 8 * 1000) / durationMs
27
+ : 0;
28
+ const m = {
29
+ bytes: timing.bytesTransferred,
30
+ durationMs,
31
+ speedBps,
32
+ };
33
+ allMeasurements.push(m);
34
+ if (speedBps > 0) {
35
+ liveSpeed = liveSpeed === 0 ? speedBps : ema(speedBps, liveSpeed);
36
+ }
37
+ if (durationMs > FINISH_REQUEST_DURATION_MS) {
38
+ phaseHitThreshold = true;
39
+ }
40
+ completedRequests++;
41
+ onProgress?.(completedRequests / totalRequests, liveSpeed);
42
+ return m;
43
+ }).catch(() => {
44
+ completedRequests++;
45
+ onProgress?.(completedRequests / totalRequests, liveSpeed);
46
+ return null;
47
+ }));
48
+ await Promise.all(promises);
49
+ remaining -= batch;
50
+ }
51
+ if (phaseHitThreshold)
52
+ break;
53
+ }
54
+ const speedBps = computeBandwidth(allMeasurements);
55
+ return {
56
+ measurements: allMeasurements,
57
+ speedBps,
58
+ speedMbps: bpsToMbps(speedBps),
59
+ };
60
+ }
61
+ //# sourceMappingURL=upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../../src/tester/upload.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare function getSpinnerFrame(): string;
2
+ export declare function resetSpinner(): void;
3
+ export declare function progressBar(progress: number, width?: number): string;
4
+ export declare function box(lines: string[], width?: number): string;
@@ -0,0 +1,39 @@
1
+ import chalk from 'chalk';
2
+ import cliSpinners from 'cli-spinners';
3
+ import { symbols, colors } from './theme.js';
4
+ const spinner = cliSpinners.dots;
5
+ let spinnerFrame = 0;
6
+ export function getSpinnerFrame() {
7
+ const frame = spinner.frames[spinnerFrame % spinner.frames.length];
8
+ spinnerFrame++;
9
+ return chalk.yellow(frame);
10
+ }
11
+ export function resetSpinner() {
12
+ spinnerFrame = 0;
13
+ }
14
+ export function progressBar(progress, width = 30) {
15
+ const clamped = Math.max(0, Math.min(1, progress));
16
+ const filled = Math.round(clamped * width);
17
+ const empty = width - filled;
18
+ return colors.accent(symbols.bar.repeat(filled)) + colors.dim(symbols.barEmpty.repeat(empty));
19
+ }
20
+ export function box(lines, width = 40) {
21
+ const inner = width - 2;
22
+ const top = colors.box(` ${symbols.boxTopLeft}${symbols.boxHorizontal.repeat(inner)}${symbols.boxTopRight}`);
23
+ const bottom = colors.box(` ${symbols.boxBottomLeft}${symbols.boxHorizontal.repeat(inner)}${symbols.boxBottomRight}`);
24
+ const rows = lines.map(line => {
25
+ const stripped = stripAnsi(line);
26
+ const pad = Math.max(0, inner - stripped.length);
27
+ const left = Math.floor(pad / 2);
28
+ const right = pad - left;
29
+ return colors.box(` ${symbols.boxVertical}`) +
30
+ ' '.repeat(left) + line + ' '.repeat(right) +
31
+ colors.box(symbols.boxVertical);
32
+ });
33
+ return [top, ...rows, bottom].join('\n');
34
+ }
35
+ function stripAnsi(str) {
36
+ // eslint-disable-next-line no-control-regex
37
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
38
+ }
39
+ //# sourceMappingURL=components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.js","sourceRoot":"","sources":["../../../src/ui/components.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,WAAW,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC;AACjC,IAAI,YAAY,GAAG,CAAC,CAAC;AAErB,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAE,CAAC;IACpE,YAAY,EAAE,CAAC;IACf,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,YAAY,GAAG,CAAC,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,KAAK,GAAG,EAAE;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7B,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AAChG,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,KAAe,EAAE,KAAK,GAAG,EAAE;IAC7C,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC;IACxB,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CACpB,MAAM,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,WAAW,EAAE,CACvF,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CACvB,MAAM,OAAO,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,cAAc,EAAE,CAC7F,CAAC;IAEF,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAC5B,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,GAAG,GAAG,IAAI,CAAC;QACzB,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;YAC5C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;YAC3C,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,4CAA4C;IAC5C,OAAO,GAAG,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { LiveUpdate } from '../tester/types.js';
2
+ export declare function composeFrame(state: LiveUpdate): string;
3
+ export declare function composePlainResult(state: LiveUpdate): string;
4
+ export declare function composeJson(state: LiveUpdate): string;
@@ -0,0 +1,111 @@
1
+ import { symbols, colors, latencyColor } from './theme.js';
2
+ import { box, progressBar, getSpinnerFrame } from './components.js';
3
+ import { formatSpeed, formatLatency } from '../stats/units.js';
4
+ const VERSION = '1.0.0';
5
+ function padRight(str, len) {
6
+ const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
7
+ const pad = Math.max(0, len - stripped.length);
8
+ return str + ' '.repeat(pad);
9
+ }
10
+ export function composeFrame(state) {
11
+ const lines = [];
12
+ // Header box
13
+ const headerLines = [
14
+ colors.header(`wirespeed`) + colors.dim(` v${VERSION}`),
15
+ ];
16
+ if (state.server) {
17
+ headerLines.push(colors.dim(`Server: Cloudflare ${state.server.colo} (${state.server.city})`));
18
+ }
19
+ lines.push(box(headerLines));
20
+ lines.push('');
21
+ // Phase status
22
+ if (state.phase === 'init') {
23
+ lines.push(` ${getSpinnerFrame()} ${colors.dim('Connecting to server...')}`);
24
+ }
25
+ else if (state.phase === 'latency') {
26
+ lines.push(` ${getSpinnerFrame()} ${colors.dim('Measuring latency...')}`);
27
+ }
28
+ else if (state.phase === 'download') {
29
+ lines.push(` ${getSpinnerFrame()} ${colors.dim('Testing download speed...')}`);
30
+ }
31
+ else if (state.phase === 'upload') {
32
+ lines.push(` ${getSpinnerFrame()} ${colors.dim('Testing upload speed...')}`);
33
+ }
34
+ lines.push('');
35
+ // Latency
36
+ if (state.latency) {
37
+ const lc = latencyColor(state.latency.median);
38
+ lines.push(` ${symbols.latency} ${colors.label('Latency')} ${padRight(lc(formatLatency(state.latency.median)), 16)}` +
39
+ colors.dim(`jitter: ${formatLatency(state.latency.jitter)}`));
40
+ }
41
+ else if (state.phase === 'latency') {
42
+ lines.push(` ${symbols.latency} ${colors.label('Latency')} ${colors.dim('measuring...')}`);
43
+ }
44
+ // Download
45
+ if (state.download) {
46
+ lines.push(` ${symbols.download} ${colors.label('Download')} ${colors.download(formatSpeed(state.download.speedBps))} ${colors.success(symbols.check)}`);
47
+ }
48
+ else if (state.phase === 'download') {
49
+ const speedStr = state.currentSpeed > 0
50
+ ? colors.download(formatSpeed(state.currentSpeed))
51
+ : colors.dim('—');
52
+ lines.push(` ${symbols.download} ${colors.label('Download')} ${speedStr}`);
53
+ lines.push(` ${progressBar(state.progress)} ${colors.dim(`${Math.round(state.progress * 100)}%`)}`);
54
+ }
55
+ else if (state.phase !== 'init' && state.phase !== 'latency') {
56
+ lines.push(` ${symbols.download} ${colors.label('Download')} ${colors.dim('—')}`);
57
+ }
58
+ // Upload
59
+ if (state.upload) {
60
+ lines.push(` ${symbols.upload} ${colors.label('Upload')} ${colors.upload(formatSpeed(state.upload.speedBps))} ${colors.success(symbols.check)}`);
61
+ }
62
+ else if (state.phase === 'upload') {
63
+ const speedStr = state.currentSpeed > 0
64
+ ? colors.upload(formatSpeed(state.currentSpeed))
65
+ : colors.dim('—');
66
+ lines.push(` ${symbols.upload} ${colors.label('Upload')} ${speedStr}`);
67
+ lines.push(` ${progressBar(state.progress)} ${colors.dim(`${Math.round(state.progress * 100)}%`)}`);
68
+ }
69
+ else if (state.phase === 'done') {
70
+ lines.push(` ${symbols.upload} ${colors.label('Upload')} ${colors.dim('—')}`);
71
+ }
72
+ // Done
73
+ if (state.phase === 'done') {
74
+ lines.push('');
75
+ }
76
+ return lines.join('\n');
77
+ }
78
+ export function composePlainResult(state) {
79
+ const lines = [];
80
+ if (state.server) {
81
+ lines.push(`Server: Cloudflare ${state.server.colo} (${state.server.city})`);
82
+ }
83
+ if (state.latency) {
84
+ lines.push(`Latency: ${formatLatency(state.latency.median)} (jitter: ${formatLatency(state.latency.jitter)})`);
85
+ }
86
+ if (state.download) {
87
+ lines.push(`Download: ${formatSpeed(state.download.speedBps)}`);
88
+ }
89
+ if (state.upload) {
90
+ lines.push(`Upload: ${formatSpeed(state.upload.speedBps)}`);
91
+ }
92
+ return lines.join('\n');
93
+ }
94
+ export function composeJson(state) {
95
+ return JSON.stringify({
96
+ server: state.server,
97
+ latency: state.latency ? {
98
+ median_ms: Math.round(state.latency.median * 100) / 100,
99
+ jitter_ms: Math.round(state.latency.jitter * 100) / 100,
100
+ } : null,
101
+ download: state.download ? {
102
+ speed_bps: Math.round(state.download.speedBps),
103
+ speed_mbps: Math.round(state.download.speedMbps * 100) / 100,
104
+ } : null,
105
+ upload: state.upload ? {
106
+ speed_bps: Math.round(state.upload.speedBps),
107
+ speed_mbps: Math.round(state.upload.speedMbps * 100) / 100,
108
+ } : null,
109
+ }, null, 2);
110
+ }
111
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../src/ui/layout.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { LiveUpdate } from '../tester/types.js';
2
+ export declare class Renderer {
3
+ private intervalId;
4
+ private state;
5
+ start(): void;
6
+ update(patch: Partial<LiveUpdate>): void;
7
+ stop(): void;
8
+ getState(): LiveUpdate;
9
+ }
@@ -0,0 +1,36 @@
1
+ import logUpdate from 'log-update';
2
+ import { RENDER_INTERVAL_MS } from '../config.js';
3
+ import { composeFrame } from './layout.js';
4
+ import { resetSpinner } from './components.js';
5
+ export class Renderer {
6
+ intervalId = null;
7
+ state = {
8
+ phase: 'init',
9
+ progress: 0,
10
+ currentSpeed: 0,
11
+ };
12
+ start() {
13
+ resetSpinner();
14
+ this.intervalId = setInterval(() => {
15
+ logUpdate(composeFrame(this.state));
16
+ }, RENDER_INTERVAL_MS);
17
+ // Render immediately
18
+ logUpdate(composeFrame(this.state));
19
+ }
20
+ update(patch) {
21
+ Object.assign(this.state, patch);
22
+ }
23
+ stop() {
24
+ if (this.intervalId) {
25
+ clearInterval(this.intervalId);
26
+ this.intervalId = null;
27
+ }
28
+ // Render final frame and persist it
29
+ logUpdate(composeFrame(this.state));
30
+ logUpdate.done();
31
+ }
32
+ getState() {
33
+ return { ...this.state };
34
+ }
35
+ }
36
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../../../src/ui/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,MAAM,OAAO,QAAQ;IACX,UAAU,GAA0C,IAAI,CAAC;IACzD,KAAK,GAAe;QAC1B,KAAK,EAAE,MAAM;QACb,QAAQ,EAAE,CAAC;QACX,YAAY,EAAE,CAAC;KAChB,CAAC;IAEF,KAAK;QACH,YAAY,EAAE,CAAC;QACf,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;YACjC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,CAAC,EAAE,kBAAkB,CAAC,CAAC;QACvB,qBAAqB;QACrB,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,CAAC,KAA0B;QAC/B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,oCAAoC;QACpC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,SAAS,CAAC,IAAI,EAAE,CAAC;IACnB,CAAC;IAED,QAAQ;QACN,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,33 @@
1
+ export declare const symbols: {
2
+ download: string;
3
+ upload: string;
4
+ latency: string;
5
+ check: string;
6
+ cross: string;
7
+ bar: string;
8
+ barEmpty: string;
9
+ boxTopLeft: string;
10
+ boxTopRight: string;
11
+ boxBottomLeft: string;
12
+ boxBottomRight: string;
13
+ boxHorizontal: string;
14
+ boxVertical: string;
15
+ bullet: string;
16
+ };
17
+ export declare const colors: {
18
+ download: import("chalk").ChalkInstance;
19
+ upload: import("chalk").ChalkInstance;
20
+ latencyGood: import("chalk").ChalkInstance;
21
+ latencyMed: import("chalk").ChalkInstance;
22
+ latencyBad: import("chalk").ChalkInstance;
23
+ label: import("chalk").ChalkInstance;
24
+ dim: import("chalk").ChalkInstance;
25
+ accent: import("chalk").ChalkInstance;
26
+ success: import("chalk").ChalkInstance;
27
+ warning: import("chalk").ChalkInstance;
28
+ error: import("chalk").ChalkInstance;
29
+ speed: import("chalk").ChalkInstance;
30
+ header: import("chalk").ChalkInstance;
31
+ box: import("chalk").ChalkInstance;
32
+ };
33
+ export declare function latencyColor(ms: number): (text: string) => string;
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+ import isUnicodeSupported from 'is-unicode-supported';
3
+ const unicode = isUnicodeSupported();
4
+ export const symbols = {
5
+ download: unicode ? '↓' : 'D',
6
+ upload: unicode ? '↑' : 'U',
7
+ latency: unicode ? '⏱' : 'P',
8
+ check: unicode ? '✓' : 'ok',
9
+ cross: unicode ? '✗' : 'x',
10
+ bar: unicode ? '█' : '#',
11
+ barEmpty: unicode ? '░' : '-',
12
+ boxTopLeft: unicode ? '╭' : '+',
13
+ boxTopRight: unicode ? '╮' : '+',
14
+ boxBottomLeft: unicode ? '╰' : '+',
15
+ boxBottomRight: unicode ? '╯' : '+',
16
+ boxHorizontal: unicode ? '─' : '-',
17
+ boxVertical: unicode ? '│' : '|',
18
+ bullet: unicode ? '•' : '*',
19
+ };
20
+ export const colors = {
21
+ download: chalk.bold.cyan,
22
+ upload: chalk.bold.magenta,
23
+ latencyGood: chalk.bold.green,
24
+ latencyMed: chalk.bold.yellow,
25
+ latencyBad: chalk.bold.red,
26
+ label: chalk.bold.white,
27
+ dim: chalk.gray,
28
+ accent: chalk.cyan,
29
+ success: chalk.green,
30
+ warning: chalk.yellow,
31
+ error: chalk.red,
32
+ speed: chalk.bold.white,
33
+ header: chalk.bold.cyan,
34
+ box: chalk.gray,
35
+ };
36
+ export function latencyColor(ms) {
37
+ if (ms < 50)
38
+ return colors.latencyGood;
39
+ if (ms < 100)
40
+ return colors.latencyMed;
41
+ return colors.latencyBad;
42
+ }
43
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../../src/ui/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAEtD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;AAErC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC7B,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC5B,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;IAC3B,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC1B,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IACxB,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC7B,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAC/B,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAChC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAClC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IACnC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAClC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;IAChC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;CAC5B,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI;IACzB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO;IAC1B,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK;IAC7B,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM;IAC7B,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG;IAC1B,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK;IACvB,GAAG,EAAE,KAAK,CAAC,IAAI;IACf,MAAM,EAAE,KAAK,CAAC,IAAI;IAClB,OAAO,EAAE,KAAK,CAAC,KAAK;IACpB,OAAO,EAAE,KAAK,CAAC,MAAM;IACrB,KAAK,EAAE,KAAK,CAAC,GAAG;IAChB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK;IACvB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI;IACvB,GAAG,EAAE,KAAK,CAAC,IAAI;CAChB,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,IAAI,EAAE,GAAG,EAAE;QAAE,OAAO,MAAM,CAAC,WAAW,CAAC;IACvC,IAAI,EAAE,GAAG,GAAG;QAAE,OAAO,MAAM,CAAC,UAAU,CAAC;IACvC,OAAO,MAAM,CAAC,UAAU,CAAC;AAC3B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@mayurpise/wirespeed",
3
+ "version": "1.0.0",
4
+ "description": "Terminal internet speed test — download, upload, and latency",
5
+ "author": "mayurpise",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mayurpise/wirespeed"
9
+ },
10
+ "homepage": "https://github.com/mayurpise/wirespeed#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/mayurpise/wirespeed/issues"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "wirespeed": "./bin/wirespeed.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "bin"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsc --watch",
25
+ "start": "node dist/src/index.js",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "keywords": [
35
+ "speed-test",
36
+ "bandwidth",
37
+ "internet",
38
+ "download",
39
+ "upload",
40
+ "latency",
41
+ "ping",
42
+ "cli",
43
+ "terminal",
44
+ "cloudflare",
45
+ "wirespeed"
46
+ ],
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "chalk": "^5.4.1",
50
+ "cli-spinners": "^3.2.0",
51
+ "is-unicode-supported": "^2.1.0",
52
+ "log-update": "^6.1.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.0.0",
56
+ "typescript": "^5.7.0"
57
+ }
58
+ }