@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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/bin/wirespeed.js +2 -0
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +25 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +71 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/meta.d.ts +2 -0
- package/dist/src/meta.js +69 -0
- package/dist/src/meta.js.map +1 -0
- package/dist/src/orchestrator.d.ts +7 -0
- package/dist/src/orchestrator.js +38 -0
- package/dist/src/orchestrator.js.map +1 -0
- package/dist/src/stats/calculator.d.ts +6 -0
- package/dist/src/stats/calculator.js +41 -0
- package/dist/src/stats/calculator.js.map +1 -0
- package/dist/src/stats/units.d.ts +4 -0
- package/dist/src/stats/units.js +25 -0
- package/dist/src/stats/units.js.map +1 -0
- package/dist/src/tester/download.d.ts +2 -0
- package/dist/src/tester/download.js +59 -0
- package/dist/src/tester/download.js.map +1 -0
- package/dist/src/tester/http-client.d.ts +3 -0
- package/dist/src/tester/http-client.js +72 -0
- package/dist/src/tester/http-client.js.map +1 -0
- package/dist/src/tester/latency.d.ts +2 -0
- package/dist/src/tester/latency.js +25 -0
- package/dist/src/tester/latency.js.map +1 -0
- package/dist/src/tester/types.d.ts +44 -0
- package/dist/src/tester/types.js +2 -0
- package/dist/src/tester/types.js.map +1 -0
- package/dist/src/tester/upload.d.ts +2 -0
- package/dist/src/tester/upload.js +61 -0
- package/dist/src/tester/upload.js.map +1 -0
- package/dist/src/ui/components.d.ts +4 -0
- package/dist/src/ui/components.js +39 -0
- package/dist/src/ui/components.js.map +1 -0
- package/dist/src/ui/layout.d.ts +4 -0
- package/dist/src/ui/layout.js +111 -0
- package/dist/src/ui/layout.js.map +1 -0
- package/dist/src/ui/renderer.d.ts +9 -0
- package/dist/src/ui/renderer.js +36 -0
- package/dist/src/ui/renderer.js.map +1 -0
- package/dist/src/ui/theme.d.ts +33 -0
- package/dist/src/ui/theme.js +43 -0
- package/dist/src/ui/theme.js.map +1 -0
- 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
|
package/bin/wirespeed.js
ADDED
|
@@ -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"}
|
package/dist/src/meta.js
ADDED
|
@@ -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,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,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,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,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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/tester/types.ts"],"names":[],"mappings":""}
|
|
@@ -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":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAG9C,SAAS,eAAe,CAAC,KAAa;IACpC,wDAAwD;IACxD,OAAO,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAAgE;IAEhE,MAAM,eAAe,GAAkB,EAAE,CAAC;IAC1C,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAE1B,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,aAAa,IAAI,KAAK,CAAC,KAAK,CAAC;IAC/B,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC7C,IAAI,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC;QAC5B,IAAI,iBAAiB,GAAG,KAAK,CAAC;QAE9B,OAAO,SAAS,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;YAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAClD,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;gBAC5C,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC;gBAC3E,MAAM,QAAQ,GAAG,UAAU,GAAG,CAAC;oBAC7B,CAAC,CAAC,CAAC,MAAM,CAAC,gBAAgB,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,UAAU;oBACnD,CAAC,CAAC,CAAC,CAAC;gBAEN,MAAM,CAAC,GAAgB;oBACrB,KAAK,EAAE,MAAM,CAAC,gBAAgB;oBAC9B,UAAU;oBACV,QAAQ;iBACT,CAAC;gBACF,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAExB,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;oBACjB,SAAS,GAAG,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;gBACpE,CAAC;gBACD,IAAI,UAAU,GAAG,0BAA0B,EAAE,CAAC;oBAC5C,iBAAiB,GAAG,IAAI,CAAC;gBAC3B,CAAC;gBAED,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC,iBAAiB,GAAG,aAAa,EAAE,SAAS,CAAC,CAAC;gBAC3D,OAAO,CAAC,CAAC;YACX,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;gBACZ,iBAAiB,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC,iBAAiB,GAAG,aAAa,EAAE,SAAS,CAAC,CAAC;gBAC3D,OAAO,IAAI,CAAC;YACd,CAAC,CAAC,CACH,CAAC;YAEF,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC5B,SAAS,IAAI,KAAK,CAAC;QACrB,CAAC;QAED,IAAI,iBAAiB;YAAE,MAAM;IAC/B,CAAC;IAED,MAAM,QAAQ,GAAG,gBAAgB,CAAC,eAAe,CAAC,CAAC;IACnD,OAAO;QACL,YAAY,EAAE,eAAe;QAC7B,QAAQ;QACR,SAAS,EAAE,SAAS,CAAC,QAAQ,CAAC;KAC/B,CAAC;AACJ,CAAC"}
|
|
@@ -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,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,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
|
+
}
|