windscout 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.js +44 -11
- package/dist/config.js +3 -0
- package/dist/tui/picker.js +34 -11
- package/dist/tui/report.js +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Run `windscout` with no arguments to open the interactive picker:
|
|
|
59
59
|
|
|
60
60
|
Your selection is saved to `~/.config/windscout/config.json` (`%USERPROFILE%\.config\windscout\config.json` on Windows) and preselected next time.
|
|
61
61
|
|
|
62
|
-
While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting, measuring latency or measuring speed). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal.
|
|
62
|
+
While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting, measuring latency or measuring speed). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal. It ends with the best connection and the other working ones, each with its `windscribe-cli connect` command; type a number at the prompt to connect to it.
|
|
63
63
|
|
|
64
64
|
### Non-interactive
|
|
65
65
|
|
|
@@ -75,7 +75,7 @@ windscout -t 40 -a # longer connect timeout, also list
|
|
|
75
75
|
|---|---|---|
|
|
76
76
|
| `-c, --countries` | comma-separated ISO country codes, city names or nicknames | open the picker |
|
|
77
77
|
| `-p, --protocols` | comma-separated: `wireguard`, `ikev2` (macOS and Windows), `stealth`, `wstunnel`, `udp`, `tcp`, optionally `:port` | saved, or all |
|
|
78
|
-
| `-t, --timeout` | seconds to wait for each connection | 20 |
|
|
78
|
+
| `-t, --timeout` | seconds to wait for each connection, at least 10 | picker: saved, or 20; with `-c`: 20 |
|
|
79
79
|
| `-a, --all` | include failed attempts in the final table | off |
|
|
80
80
|
| `-n, --no-speed` | connect-only mode: skip the download test, rank by latency | picker: saved, or connect-only on first run; with `-c`: speed test on |
|
|
81
81
|
| `-f, --firewall` | block internet while scanning, except through the VPN | picker: saved, or off; with `-c`: off |
|
package/dist/cli.js
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
// WindScout: find which Windscribe locations and protocols actually work from here.
|
|
3
3
|
// Drives the official windscribe-cli: connects to each country/protocol pair,
|
|
4
4
|
// measures latency and download speed through the tunnel, prints a ranked table.
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
6
|
-
import { CONFIG, loadConfig } from "./config.js";
|
|
7
|
+
import { CONFIG, DEFAULT_TIMEOUT, loadConfig, MIN_TIMEOUT } from "./config.js";
|
|
7
8
|
import { Scan } from "./scan.js";
|
|
8
9
|
import { pick } from "./tui/picker.js";
|
|
9
10
|
import { logProgress, showProgress } from "./tui/progress.js";
|
|
10
|
-
import { report } from "./tui/report.js";
|
|
11
|
+
import { command, report, target } from "./tui/report.js";
|
|
11
12
|
import { leaveScreen, out } from "./tui/terminal.js";
|
|
12
|
-
import { yellow } from "./tui/text.js";
|
|
13
|
+
import { cyan, green, yellow } from "./tui/text.js";
|
|
14
|
+
import { now, sleep } from "./time.js";
|
|
13
15
|
import { checkUpdate, updateNotice, VERSION } from "./update.js";
|
|
14
|
-
import { blockInternet, CLI, isFirewallOn, PROTOCOLS, restoreFirewall, waitForLogin } from "./windscribe.js";
|
|
16
|
+
import { blockInternet, CLI, connectState, isFirewallOn, PROTOCOLS, restoreFirewall, waitForLogin, windscribe } from "./windscribe.js";
|
|
15
17
|
const HELP = `Usage: windscout [options]
|
|
16
18
|
|
|
17
19
|
Find which Windscribe locations and protocols work from your network, ranked by speed.
|
|
@@ -20,7 +22,7 @@ Without -c it opens an interactive country picker (your selection is remembered)
|
|
|
20
22
|
Options:
|
|
21
23
|
-c, --countries LIST comma-separated ISO codes, city names or nicknames (skips the picker)
|
|
22
24
|
-p, --protocols LIST comma-separated protocols, optional :port (default: ${PROTOCOLS.join(",")})
|
|
23
|
-
-t, --timeout SEC seconds to wait for each connection (default:
|
|
25
|
+
-t, --timeout SEC seconds to wait for each connection, at least ${MIN_TIMEOUT} (default: saved, or ${DEFAULT_TIMEOUT})
|
|
24
26
|
-a, --all also show failed attempts in the final table
|
|
25
27
|
-n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
|
|
26
28
|
-f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
|
|
@@ -31,12 +33,39 @@ Selection is stored in ${CONFIG}
|
|
|
31
33
|
Example: windscout -c DE,NL,TR -p stealth,tcp:443`;
|
|
32
34
|
// "DE, NL,,TR" → ["DE", "NL", "TR"]
|
|
33
35
|
const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
36
|
+
const ask = (question) => new Promise((resolve) => {
|
|
37
|
+
const rl = createInterface({ input: process.stdin, output: out });
|
|
38
|
+
rl.on("SIGINT", () => rl.close()); // Ctrl-C / Ctrl-D: same as enter
|
|
39
|
+
rl.on("close", () => resolve(""));
|
|
40
|
+
rl.question(question, (a) => (resolve(a.trim()), rl.close()));
|
|
41
|
+
});
|
|
42
|
+
/** Asks which numbered connection to make, then runs its command and waits for it to connect */
|
|
43
|
+
async function offerConnect(choices, timeout) {
|
|
44
|
+
const others = choices.length > 1 ? `, 1-${choices.length - 1} = other` : "";
|
|
45
|
+
for (;;) {
|
|
46
|
+
const a = await ask(`\nconnect to # (0 = best${others}, enter to quit): `);
|
|
47
|
+
if (!a)
|
|
48
|
+
return;
|
|
49
|
+
const r = /^\d+$/.test(a) ? choices[Number(a)] : undefined;
|
|
50
|
+
if (!r) {
|
|
51
|
+
console.log(yellow(`no #${a} in the tables`));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
console.log(cyan(command(r)));
|
|
55
|
+
await windscribe("connect", target(r), r.protocol);
|
|
56
|
+
let state = "";
|
|
57
|
+
for (const t = now(); now() - t < timeout && !(state = await connectState()).startsWith("Connected");)
|
|
58
|
+
await sleep(1000);
|
|
59
|
+
console.log(state.startsWith("Connected") ? green(state) : yellow(`${state || "not connected"} after ${timeout} s`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
34
63
|
async function main() {
|
|
35
64
|
const { values: a } = parseArgs({
|
|
36
65
|
options: {
|
|
37
66
|
countries: { type: "string", short: "c" },
|
|
38
67
|
protocols: { type: "string", short: "p" },
|
|
39
|
-
timeout: { type: "string", short: "t"
|
|
68
|
+
timeout: { type: "string", short: "t" },
|
|
40
69
|
all: { type: "boolean", short: "a", default: false },
|
|
41
70
|
help: { type: "boolean", short: "h", default: false },
|
|
42
71
|
version: { type: "boolean", short: "v", default: false },
|
|
@@ -55,9 +84,9 @@ async function main() {
|
|
|
55
84
|
console.error(yellow(notice));
|
|
56
85
|
return;
|
|
57
86
|
}
|
|
58
|
-
|
|
59
|
-
if (!(timeout
|
|
60
|
-
throw new Error(
|
|
87
|
+
let timeout = a.timeout ? Number(a.timeout) : undefined;
|
|
88
|
+
if (timeout !== undefined && !(timeout >= MIN_TIMEOUT))
|
|
89
|
+
throw new Error(`--timeout must be at least ${MIN_TIMEOUT} seconds`);
|
|
61
90
|
const status = await waitForLogin();
|
|
62
91
|
const tty = Boolean(process.stdin.isTTY && out.isTTY);
|
|
63
92
|
let countries = a.countries ? list(a.countries) : undefined;
|
|
@@ -71,7 +100,7 @@ async function main() {
|
|
|
71
100
|
if (protocols && saved)
|
|
72
101
|
saved.protocols = protocols.filter((p) => PROTOCOLS.includes(p));
|
|
73
102
|
const choice = await pick(saved, {
|
|
74
|
-
timeout,
|
|
103
|
+
timeout: timeout ?? saved?.timeout ?? DEFAULT_TIMEOUT,
|
|
75
104
|
speedTest: speedTest && (saved?.speed ?? false), // first run: connect-only
|
|
76
105
|
// already on in the app: show it on, since the scan won't turn it off
|
|
77
106
|
firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
|
|
@@ -82,8 +111,10 @@ async function main() {
|
|
|
82
111
|
protocols ??= choice.protocols;
|
|
83
112
|
speedTest = choice.speed;
|
|
84
113
|
firewall = choice.firewall;
|
|
114
|
+
timeout = choice.timeout;
|
|
85
115
|
}
|
|
86
116
|
protocols ??= PROTOCOLS;
|
|
117
|
+
timeout ??= DEFAULT_TIMEOUT;
|
|
87
118
|
const scan = new Scan(countries, protocols, { timeout, speedTest });
|
|
88
119
|
const blocking = firewall && (await blockInternet());
|
|
89
120
|
const view = tty ? showProgress(scan, blocking) : logProgress(scan);
|
|
@@ -92,11 +123,13 @@ async function main() {
|
|
|
92
123
|
view.close();
|
|
93
124
|
if (scan.stopping)
|
|
94
125
|
console.log(yellow("interrupted, showing partial results"));
|
|
95
|
-
report(scan.results, protocols, { all: a.all, speedTest });
|
|
126
|
+
const choices = report(scan.results, protocols, { all: a.all, speedTest });
|
|
96
127
|
await checkUpdate();
|
|
97
128
|
const notice = updateNotice();
|
|
98
129
|
if (notice)
|
|
99
130
|
console.error("\n" + yellow(notice));
|
|
131
|
+
if (tty && choices.length)
|
|
132
|
+
await offerConnect(choices, timeout);
|
|
100
133
|
}
|
|
101
134
|
main().catch(async (e) => {
|
|
102
135
|
leaveScreen();
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,8 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { COUNTRIES, PROTOCOLS } from "./windscribe.js";
|
|
6
|
+
export const DEFAULT_TIMEOUT = 20;
|
|
7
|
+
export const MIN_TIMEOUT = 10; // seconds; shorter cuts off connections that would have worked
|
|
6
8
|
export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
|
|
7
9
|
/** The saved choice, minus anything this version doesn't offer; null on first run or an unreadable file */
|
|
8
10
|
export function loadConfig() {
|
|
@@ -13,6 +15,7 @@ export function loadConfig() {
|
|
|
13
15
|
protocols: (c.protocols ?? []).filter((x) => PROTOCOLS.includes(x)),
|
|
14
16
|
speed: c.speed !== false,
|
|
15
17
|
firewall: c.firewall === true,
|
|
18
|
+
timeout: c.timeout >= MIN_TIMEOUT ? c.timeout : DEFAULT_TIMEOUT,
|
|
16
19
|
};
|
|
17
20
|
}
|
|
18
21
|
catch {
|
package/dist/tui/picker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { saveConfig } from "../config.js";
|
|
1
|
+
import { MIN_TIMEOUT, saveConfig } from "../config.js";
|
|
2
2
|
import { attemptSeconds, estimate } from "../scan.js";
|
|
3
3
|
import { duration } from "../time.js";
|
|
4
4
|
import { checkUpdate, updateNotice } from "../update.js";
|
|
@@ -13,6 +13,7 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
13
13
|
const selected = new Set(saved?.countries ?? []);
|
|
14
14
|
const protos = new Set(saved?.protocols.length ? saved.protocols : PROTOCOLS);
|
|
15
15
|
let cursor = 0, top = 0, message = "", query = "", searching = false;
|
|
16
|
+
let seconds = String(timeout), editing = false; // the timeout, as typed
|
|
16
17
|
let hit;
|
|
17
18
|
const visible = () => {
|
|
18
19
|
const q = query.toLowerCase();
|
|
@@ -24,7 +25,7 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
24
25
|
const rows = out.rows || 24;
|
|
25
26
|
const notice = updateNotice();
|
|
26
27
|
const matches = visible();
|
|
27
|
-
const height = Math.max(3, rows -
|
|
28
|
+
const height = Math.max(3, rows - 21 - (notice ? 1 : 0));
|
|
28
29
|
cursor = Math.max(0, Math.min(cursor, matches.length - 1));
|
|
29
30
|
if (cursor < top)
|
|
30
31
|
top = cursor;
|
|
@@ -39,6 +40,9 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
39
40
|
const fwLine = `${dim(under("f"))} ${firewall ? green("● " + fwLabel) : dim("○ " + fwLabel)} ${dim(firewall
|
|
40
41
|
? "only VPN traffic gets through"
|
|
41
42
|
: "off: apps may leak in between")}`;
|
|
43
|
+
timeout = Number(seconds);
|
|
44
|
+
const short = timeout < MIN_TIMEOUT;
|
|
45
|
+
const timeoutLine = `${dim(under("t"))} timeout ${editing ? bold(seconds) + cyan("▏") : (short ? yellow : bold)(seconds || "?")} s ${dim(`per connection · at least ${MIN_TIMEOUT}`)}`;
|
|
42
46
|
const search = searching
|
|
43
47
|
? `${cyan("🔍")} ${bold(query)}${cyan("▏")}`
|
|
44
48
|
: query ? `🔍 ${query}` : dim("🔍 press / to search");
|
|
@@ -59,9 +63,9 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
59
63
|
["quit", under("Q") + "uit"],
|
|
60
64
|
];
|
|
61
65
|
const buttonLine = buttons.map(([id, label]) => (id === "start" ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
|
|
62
|
-
const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine), vw(fwLine)) + 4 });
|
|
66
|
+
const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine), vw(fwLine), vw(timeoutLine)) + 4 });
|
|
63
67
|
const width = vw(table[0]);
|
|
64
|
-
const head = box([protoLine, speedLine, fwLine], {
|
|
68
|
+
const head = box([protoLine, speedLine, fwLine, timeoutLine], {
|
|
65
69
|
title: bold(cyan("WindScout")),
|
|
66
70
|
right: `${bold(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
67
71
|
width: width - 4,
|
|
@@ -76,10 +80,12 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
76
80
|
centerIn(n
|
|
77
81
|
? `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(estimate(selected.size, protos.size, timeout)))} ${dim(`(~${attemptSeconds(timeout)}s each)`)}`
|
|
78
82
|
: yellow("select at least one country and one protocol"), width),
|
|
79
|
-
centerIn(dim(
|
|
80
|
-
? "type to
|
|
81
|
-
:
|
|
82
|
-
|
|
83
|
+
centerIn(dim(editing
|
|
84
|
+
? "type the seconds to wait for each connection · enter/esc done"
|
|
85
|
+
: searching
|
|
86
|
+
? "type to filter · ↑↓ move · enter/esc done"
|
|
87
|
+
: `↑↓ move · space select · a/n ${query ? "all/none shown" : "all/none"} · / search`), width),
|
|
88
|
+
centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · s speed test · f firewall · t timeout · enter start · ${query ? "esc clear · " : ""}q quit`), width),
|
|
83
89
|
];
|
|
84
90
|
if (message)
|
|
85
91
|
lines.push(centerIn(yellow(message), width));
|
|
@@ -103,7 +109,7 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
103
109
|
return range;
|
|
104
110
|
});
|
|
105
111
|
hit = {
|
|
106
|
-
protoRow: at.top + 1, protoHits, speedRow: at.top + 2, firewallRow: at.top + 3,
|
|
112
|
+
protoRow: at.top + 1, protoHits, speedRow: at.top + 2, firewallRow: at.top + 3, timeoutRow: at.top + 4,
|
|
107
113
|
searchRow: at.top + head.length + 1,
|
|
108
114
|
firstRow: at.top + head.length + 5, shown: shown.length,
|
|
109
115
|
buttonRow: at.top + head.length + table.length + 1, buttonHits,
|
|
@@ -125,12 +131,17 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
125
131
|
message = "nothing to scan yet";
|
|
126
132
|
return false;
|
|
127
133
|
}
|
|
134
|
+
if (!(timeout >= MIN_TIMEOUT)) {
|
|
135
|
+
message = `the timeout must be at least ${MIN_TIMEOUT} seconds`;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
128
138
|
// keep the user's order of preference stable: list order
|
|
129
139
|
const choice = {
|
|
130
140
|
countries: COUNTRIES.filter((c) => selected.has(c)),
|
|
131
141
|
protocols: PROTOCOLS.filter((p) => protos.has(p)),
|
|
132
142
|
speed: speedTest,
|
|
133
143
|
firewall,
|
|
144
|
+
timeout,
|
|
134
145
|
};
|
|
135
146
|
saveConfig(choice);
|
|
136
147
|
finish(choice);
|
|
@@ -149,9 +160,11 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
149
160
|
else if (m.button !== 0)
|
|
150
161
|
return; // left button only
|
|
151
162
|
else if (y === hit.searchRow)
|
|
152
|
-
searching = true;
|
|
163
|
+
searching = true, editing = false;
|
|
164
|
+
else if (y === hit.timeoutRow)
|
|
165
|
+
editing = true, searching = false;
|
|
153
166
|
else {
|
|
154
|
-
searching = false; // clicking anywhere else ends typing, like leaving a text field
|
|
167
|
+
searching = editing = false; // clicking anywhere else ends typing, like leaving a text field
|
|
155
168
|
if (y >= hit.firstRow && y < hit.firstRow + hit.shown) {
|
|
156
169
|
cursor = top + (y - hit.firstRow);
|
|
157
170
|
const c = matches[cursor];
|
|
@@ -203,6 +216,14 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
203
216
|
cursor = Math.max(0, cursor - 10);
|
|
204
217
|
else if (k === "pagedown")
|
|
205
218
|
cursor = Math.min(matches.length - 1, cursor + 10);
|
|
219
|
+
else if (editing) {
|
|
220
|
+
if (k === "return" || k === "escape")
|
|
221
|
+
editing = false;
|
|
222
|
+
else if (k === "backspace")
|
|
223
|
+
seconds = seconds.slice(0, -1);
|
|
224
|
+
else if (/^[0-9]$/.test(str ?? "") && seconds.length < 4)
|
|
225
|
+
seconds = seconds === "0" ? str : seconds + str;
|
|
226
|
+
}
|
|
206
227
|
else if (searching) {
|
|
207
228
|
// typing goes into the search box; letters are not shortcuts here
|
|
208
229
|
if (k === "return" || k === "escape")
|
|
@@ -234,6 +255,8 @@ export function pick(saved, { timeout, speedTest, firewall }) {
|
|
|
234
255
|
speedTest = !speedTest;
|
|
235
256
|
else if (k === "f")
|
|
236
257
|
firewall = !firewall;
|
|
258
|
+
else if (k === "t")
|
|
259
|
+
editing = true, seconds = "";
|
|
237
260
|
else if (/^[1-9]$/.test(str ?? "") && PROTOCOLS[Number(str) - 1])
|
|
238
261
|
flip(protos, PROTOCOLS[Number(str) - 1]);
|
|
239
262
|
else if (k === "escape" && query)
|
package/dist/tui/report.js
CHANGED
|
@@ -5,26 +5,35 @@ import { flag } from "./country.js";
|
|
|
5
5
|
import { matrix, resultTable } from "./tables.js";
|
|
6
6
|
import { bold, cyan, dim, fmt, green, vw, yellow } from "./text.js";
|
|
7
7
|
import { box, grid } from "./widgets.js";
|
|
8
|
+
// nickname pins the exact datacenter
|
|
9
|
+
export const target = (r) => r.location.split(" - ").at(-1) || r.query;
|
|
10
|
+
export const command = (r) => `${CLI} connect "${target(r)}" ${r.protocol}`;
|
|
11
|
+
/** Prints the report; returns the connections it numbered: the best (#0), then the others (#1…) */
|
|
8
12
|
export function report(results, protocols, { all, speedTest }) {
|
|
9
13
|
const ok = sortOk(results);
|
|
10
14
|
const rows = [...ok, ...(all ? results.filter((r) => r.status !== "ok") : [])];
|
|
15
|
+
const others = ok.slice(1).sort((x, y) => (x.latencyMs ?? Infinity) - (y.latencyMs ?? Infinity));
|
|
11
16
|
// render once to learn the widest table, then again so every box lines up
|
|
12
17
|
const build = (minWidth) => ({
|
|
13
18
|
avail: matrix(results, protocols, { minWidth }),
|
|
14
19
|
main: rows.length
|
|
15
20
|
? resultTable(rows, speedTest, { caption: bold("Results"), captionRight: dim(`${ok.length} working`), minWidth })
|
|
16
21
|
: [],
|
|
22
|
+
others: others.length
|
|
23
|
+
? grid([{ title: "#", align: "right" }, { title: "Location" }, { title: "Protocol" }, { title: "Latency", align: "right" }, { title: "Connect with" }], others.map((r, i) => [dim(String(i + 1)), `${flag(r.query)} ${r.location}`, r.protocol,
|
|
24
|
+
r.latencyMs === null ? "-" : `${fmt(r.latencyMs, 0)} ms`, cyan(command(r))]), { caption: bold("Other working locations"), captionRight: dim("by latency"), minWidth })
|
|
25
|
+
: [],
|
|
17
26
|
});
|
|
18
27
|
const first = build(0);
|
|
19
|
-
const width = Math.max(64, ...[first.avail[0], first.main[0]].filter(Boolean).map(vw));
|
|
20
|
-
const { avail, main } = build(width);
|
|
28
|
+
const width = Math.max(64, ...[first.avail[0], first.main[0], first.others[0]].filter(Boolean).map(vw));
|
|
29
|
+
const { avail, main, others: rest } = build(width);
|
|
21
30
|
console.log();
|
|
22
31
|
if (avail.length)
|
|
23
32
|
console.log(avail.join("\n"));
|
|
24
33
|
if (!ok.length) {
|
|
25
34
|
console.log(box([yellow("Nothing connected."),
|
|
26
35
|
dim("Try other countries, ports (e.g. -p stealth:443,tcp:443) or a longer --timeout.")], { title: bold(cyan("WindScout")), width: width - 4 }).join("\n"));
|
|
27
|
-
return;
|
|
36
|
+
return [];
|
|
28
37
|
}
|
|
29
38
|
console.log(main.join("\n"));
|
|
30
39
|
const stats = protocols.map((p) => {
|
|
@@ -35,10 +44,12 @@ export function report(results, protocols, { all, speedTest }) {
|
|
|
35
44
|
const bestValue = (r) => (speedTest ? `${fmt(r.speedMbps, 1)} Mbps` : `${fmt(r.latencyMs, 0)} ms`);
|
|
36
45
|
console.log(grid([{ title: "Protocol" }, { title: "Worked", align: "right" }, { title: speedTest ? "Best speed" : "Best latency", align: "right" }, { title: "Best location" }], stats.map((s) => [s.p, `${s.good}/${s.tried}`, s.best ? bestValue(s.best) : "-", s.best ? `${flag(s.best.query)} ${s.best.location}` : "-"]), { caption: bold("Per protocol"), minWidth: width }).join("\n"));
|
|
37
46
|
const b = ok[0];
|
|
38
|
-
const target = b.location.split(" - ").at(-1) || b.query; // nickname pins the exact datacenter
|
|
39
47
|
const facts = [speedTest && `${fmt(b.speedMbps, 1)} Mbps`, `${fmt(b.latencyMs, 0)} ms`].filter(Boolean).join(" · ");
|
|
40
48
|
console.log(box([
|
|
41
49
|
`${flag(b.query)} ${bold(b.location)} over ${bold(b.protocol)} ${dim("· " + facts)}`,
|
|
42
|
-
cyan(
|
|
50
|
+
cyan(command(b)),
|
|
43
51
|
], { title: green(speedTest ? "★ Best" : "★ Best (lowest latency)"), width: width - 4 }).join("\n"));
|
|
52
|
+
if (rest.length)
|
|
53
|
+
console.log(rest.join("\n"));
|
|
54
|
+
return [b, ...others];
|
|
44
55
|
}
|