windscout 0.6.1 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # WindScout
2
2
 
3
- Find which Windscribe locations and protocols actually work from your network, ranked by speed. Runs on Linux, macOS and Windows.
3
+ Find which Windscribe locations and protocols actually work from your network, ranked by latency. Runs on Linux, macOS and Windows.
4
4
 
5
- WindScout drives the official `windscribe-cli`: it connects to each country × protocol pair, measures latency and download speed through the tunnel, disconnects, and prints a ranked table.
5
+ WindScout drives the official `windscribe-cli`: it connects to each country × protocol pair, measures latency through the tunnel, disconnects, and prints a ranked table.
6
6
 
7
7
  ![WindScout picking countries, scanning, and printing the availability grid and ranked results](demo.gif)
8
8
 
@@ -30,7 +30,7 @@ Run `windscout` with no arguments to open the interactive picker:
30
30
  ```
31
31
  ╭─ WindScout ──────────────────────────────────── 2/68 selected ─╮
32
32
  │ 1 ● wireguard 2 ● stealth 3 ○ wstunnel 4 ● udp 5 ● tcp │
33
- │ s ○ speed test off: connect-only, ranks by latency │
33
+ │ p ○ all ports off: each protocol on its default port │
34
34
  ╰────────────────────────────────────────────────────────────────╯
35
35
  ╭────────────────────────────────────────────────────────────────╮
36
36
  │ 🔍 an▏ 1–16 of 18 │
@@ -51,7 +51,7 @@ Run `windscout` with no arguments to open the interactive picker:
51
51
  | `/` | search by country name or code (enter/esc to finish, esc again to clear) |
52
52
  | `a` / `n` | select all / unselect all (only the matching countries while a search is active) |
53
53
  | `1`–`6` | toggle a protocol (`1`–`5` on Linux, which has no IKEv2) |
54
- | `s` | toggle the speed test (off = connect-only mode) |
54
+ | `p` | toggle [all ports](#all-ports): each protocol on every port it has, not just its default |
55
55
  | `f` | toggle "block internet while scanning" (firewall) |
56
56
  | `r` | toggle random order: scan the selected countries shuffled, a new order each run |
57
57
  | `d` | toggle the [parallel scan in docker containers](#parallel-scan-with-docker) (installs or starts docker first, after asking) |
@@ -59,11 +59,11 @@ Run `windscout` with no arguments to open the interactive picker:
59
59
  | `enter` | start scanning |
60
60
  | `q` | quit (during a scan: stop and show partial results; press again to force quit) |
61
61
 
62
- **Mouse:** click a country to select or unselect it, click a protocol or the speed-test line to toggle it, click the search box to start typing (click anywhere else to leave it), and scroll the list with the wheel. The `[ ▶ Start ] [ All ] [ None ] [ Quit ]` buttons under the table do the same as their keys; the shortcut key is underlined on screen (on the buttons, the protocol numbers and `s`). This needs a terminal with mouse support, which most have. On Windows, use Windows Terminal; if clicks do nothing there, the keyboard does everything.
62
+ **Mouse:** click a country to select or unselect it, click a protocol or a setting's line to toggle it, click the search box to start typing (click anywhere else to leave it), and scroll the list with the wheel. The `[ ▶ Start ] [ All ] [ None ] [ Quit ]` buttons under the table do the same as their keys; the shortcut key is underlined on screen (on the buttons, the protocol numbers and the settings' letters). This needs a terminal with mouse support, which most have. On Windows, use Windows Terminal; if clicks do nothing there, the keyboard does everything.
63
63
 
64
64
  Your selection is saved to `~/.config/windscout/config.json` (`%USERPROFILE%\.config\windscout\config.json` on Windows) and preselected next time.
65
65
 
66
- 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. At the prompt, press enter to connect to the best one, type another one's number to connect to that, or `q` to quit.
66
+ While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting or measuring latency). 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. At the prompt, press enter to connect to the best one, type another one's number to connect to that, or `q` to quit.
67
67
 
68
68
  ### Non-interactive
69
69
 
@@ -72,6 +72,7 @@ Pass `-c` to skip the picker, for scripts or when there is no terminal:
72
72
  ```sh
73
73
  windscout -c DE,NL,TR -p stealth,tcp:443 # chosen countries and protocols (port optional)
74
74
  windscout -c Frankfurt,Wurstchen # cities or datacenter nicknames work too
75
+ windscout -c DE -p stealth,tcp -P # every port of stealth and tcp
75
76
  windscout -t 40 -a # longer connect timeout, also list failed attempts
76
77
  ```
77
78
 
@@ -79,19 +80,17 @@ windscout -t 40 -a # longer connect timeout, also list
79
80
  |---|---|---|
80
81
  | `-c, --countries` | comma-separated ISO country codes, city names or nicknames | open the picker |
81
82
  | `-p, --protocols` | comma-separated: `wireguard`, `ikev2` (macOS and Windows), `stealth`, `wstunnel`, `udp`, `tcp`, optionally `:port` | saved, or all |
83
+ | `-P, --all-ports` | try each protocol on [every port](#all-ports) Windscribe offers for it, not just its default | picker: saved, or off; with `-c`: off |
82
84
  | `-t, --timeout` | seconds to wait for each connection, at least 10 | picker: saved, or 15; with `-c`: 15 |
83
85
  | `-a, --all` | include failed attempts in the final table | off |
84
- | `-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 |
85
86
  | `-f, --firewall` | block internet while scanning, except through the VPN | picker: saved, or off; with `-c`: off |
86
87
  | `-r, --random` | scan the countries in a random order | picker: saved, or off; with `-c`: off |
87
88
  | `-d, --docker` | [scan in parallel, in docker containers](#parallel-scan-with-docker) | picker: saved, or off; with `-c`: off |
88
89
  | `-v, --version` | print the installed version; if a newer one is on npm, show the update command (with `sudo` when needed) and ask (y/n) to run it | |
89
90
 
90
- ## Connect-only mode
91
+ ## Availability grid
91
92
 
92
- Often you only need to know **what connects**, not how fast it is. Turn the speed test off with `s` in the picker or with `--no-speed`. Each working connection is then checked with a quick latency test only. That saves about 15 s per working connection, and results are ranked by latency. Connect-only is the default on the first run, and the choice is remembered.
93
-
94
- Both modes show an **availability grid**: one row per country, and ✓ / ✗ for each protocol (`·` = not tried yet). Location(s) lists each datacenter that connected, one per line, since protocols can land on different ones:
93
+ Each working connection gets a quick latency test, and the working ones are ranked by latency. The scan screen and the final report show an **availability grid**: one row per country, and ✓ / ✗ for each protocol (`·` = not tried yet; `key limit`, in a [parallel scan](#parallel-scan-with-docker): WireGuard couldn't be tried). With [all ports](#all-ports), it counts how many of a protocol's ports worked: `✓ 3/11`. Location(s) lists each datacenter that connected, one per line, since protocols can land on different ones:
95
94
 
96
95
  ```
97
96
  ╭──────────────────────────────────────────────────────────────────────────────╮
@@ -106,7 +105,25 @@ Both modes show an **availability grid**: one row per country, and ✓ / ✗ for
106
105
  ╰─────────────────────────┴────────────────────────┴─────────┴───────────┴─────╯
107
106
  ```
108
107
 
109
- While scanning, press `tab` to switch between the availability grid and the speed or latency ranking. Press `p` (or `space`) to pause: the current attempt finishes, the VPN disconnects, and the scan waits until you press `p` again. Paused time doesn't count toward elapsed or remaining. In connect-only mode the scan starts on the availability grid.
108
+ While scanning, press `tab` to switch between the availability grid and the latency ranking. Press `p` (or `space`) to pause: the current attempt finishes, the VPN disconnects, and the scan waits until you press `p` again. Paused time doesn't count toward elapsed or remaining. The scan starts on the availability grid.
109
+
110
+ ## All ports
111
+
112
+ Each protocol is normally tried on its default port, the first one Windscribe lists for it. A network can block a protocol on one port and let it through on another, so turn on **all ports** with `p` in the picker or `-P` (`--all-ports`): each selected protocol is then tried on every port Windscribe offers for it. WindScout asks your Windscribe app for the ports (`windscribe-cli ports tcp`), so the list follows Windscribe's. In October 2026 it was:
113
+
114
+ | Protocol | Ports | Attempts per country |
115
+ |---|---|---|
116
+ | wireguard | 443, 80, 53, 123, 1194, 65142 | 6 |
117
+ | stealth | 443, 587, 21, 22, 80, 123, 143, 3306, 8080, 54783, 8443 | 11 |
118
+ | wstunnel | 443 | 1 |
119
+ | udp | 443, 80, 53, 123, 1194, 54783 | 6 |
120
+ | tcp | 443, 587, 21, 22, 80, 123, 143, 3306, 8080, 54783, 1194 | 11 |
121
+
122
+ With every protocol selected, that's 35 attempts per country instead of 5, 7 times as many (on macOS and Windows, IKEv2 adds its own ports). The picker shows the count (`35 attempts per country, not 5`) and the estimate: one country takes about 11m 40s instead of 1m 40s in a normal scan, and about 2 minutes in a [parallel scan](#parallel-scan-with-docker), which helps most here. To try fewer, select only the protocols you need, or give a port yourself: a protocol with a port (`-p tcp:443`) stays on that one.
123
+
124
+ The availability grid keeps one column per protocol and counts how many of its ports worked (`✓ 3/11`, or `✗ 0/11` when none did). The results list each working protocol with its port, and the per-protocol table has a line per port.
125
+
126
+ When the app making an attempt doesn't offer a port, Windscribe quietly connects on the protocol's default port instead. That can happen in a parallel scan, where each container runs its own copy of the app, which can be older or newer than yours. WindScout checks the port it actually connected on and shows those attempts as `no port` (in yellow), rather than as results for a port that wasn't tried. Listing ports needs Windscribe 2.22 or newer. With an older app, all ports changes nothing, and the picker says so.
110
127
 
111
128
  ## Block internet while scanning (firewall)
112
129
 
@@ -116,7 +133,7 @@ Between attempts the VPN is disconnected for a moment, and apps on your computer
116
133
 
117
134
  A normal scan tries one location at a time, because your computer has one VPN connection. Turn on **parallel scan** with `d` in the picker or `--docker`, and each attempt runs in its own docker container instead. Each container has its own Windscribe app (Windscribe's Linux CLI app) and so its own VPN connection, and several locations are tried at once. The results come back to the scan screen, which shows a line per container.
118
135
 
119
- **How many containers:** one per attempt, as many as docker's CPUs allow and three quarters of its memory (at 250 MB each), at most 8. The picker shows the number (`8 at a time`) and the estimate that goes with it.
136
+ **How many containers:** one per attempt, so 1 country with 5 protocols starts 5. Docker's CPUs, three quarters of its memory (at 250 MB each) and a maximum of 16 limit that. With more attempts than that, WindScout starts the fewest containers that need no more rounds: 17 attempts get 9 containers with 2 attempts each, the same 2 rounds that 16 containers would take. Fewer containers log in sooner (in a test, 1 took 37 s, and 16 together up to 75 s) and register fewer WireGuard keys. The picker shows the number (`9 at a time`) and the estimate that goes with it, which includes the time the containers take to log in before the scan starts (`+ ~58s login`: about 35 s, plus 2.5 s per container).
120
137
 
121
138
  **First time:** if docker isn't installed or isn't running, `d` (or a scan with `--docker`) opens a dialog that says what WindScout will do, and does it once you agree:
122
139
 
@@ -142,9 +159,11 @@ The mirrors come from [Mirava](https://github.com/MiravaOrg/Mirava)'s list of mi
142
159
 
143
160
  **Login:** the containers' Windscribe apps log in with the session that the Windscribe app on this computer is logged in with, so they never log in with your password. Windscribe locks an account that logs in many times at once, and a login per container would do that. WindScout reads the session from the app's settings (`~/.config/Windscribe/Windscribe2.conf` on Linux, the `com.windscribe.Windscribe2` defaults on macOS, `HKEY_CURRENT_USER\Software\Windscribe\Windscribe2` in the registry on Windows) and hands it to each container on its standard input, not on a command line, where other programs could see it. Each container's docker volume (`windscout-1`, `windscout-2`, …) keeps its app's settings for the next scans, with the WireGuard keys it registered with your account. A volume that another account used starts over. If you log out of the Windscribe app, the containers' session ends too, and the next scan gives them the new one.
144
161
 
162
+ **WireGuard keys:** each container's app registers a WireGuard key of its own with your account the first time it uses WireGuard, and keeps it in its volume for later scans. Windscribe allows each account only so many keys. A container past that limit can't try WireGuard, which says nothing about your network, so the grid shows `key limit` (in yellow) instead of ✗, and those attempts don't count in the report's per-protocol numbers. Under the grid, the report says how many there were and gives the command that tries them on this computer, which has a key of its own: `windscout -c "DE,NL" -p wireguard`.
163
+
145
164
  WindScout 0.6.0 asked for your username and password instead, and could save them in `~/.config/windscout/login.json`. WindScout now deletes that file.
146
165
 
147
- **Measuring:** the containers share your internet connection, so they take turns measuring latency and speed while the connecting happens in parallel. Numbers stay comparable with a normal scan. Most of the time saved comes from attempts that hang until the timeout, which overlap now.
166
+ **Measuring:** the containers share your internet connection, so they take turns measuring latency while the connecting happens in parallel. Numbers stay comparable with a normal scan. Most of the time saved comes from attempts that hang until the timeout, which overlap now.
148
167
 
149
168
  **Differences from a normal scan:**
150
169
 
@@ -164,23 +183,23 @@ While scanning:
164
183
  │ ██████████████████████████████████████████▌░░░░░░░░░░░░░░░░ 72% │
165
184
  │ │
166
185
  │ ⠏ 🇳🇱 Netherlands · stealth │
167
- │ measuring speed · Amsterdam - Tulip · 3s │
186
+ │ measuring latency · Amsterdam - Tulip · 1s │
168
187
  ├──────────────────────────────────────────────────────────────────────┤
169
188
  │ elapsed 25s │ remaining ≈ 13s │ ✓ 2 working │ ✗ 1 blocked │
170
189
  ╰──────────────────────────────────────────────────────────────────────╯
171
190
  ╭──────────────────────────────────────────────────────────────────────╮
172
191
  │ Working so far 2 found │
173
192
  ├───┬────────────┬───────────────────────┬───────────┬───────────┬─────┤
174
- │ # │ Country │ Location │ Protocol │ Speed │ ... │
193
+ │ # │ Country │ Location │ Protocol │ Latency │ ... │
175
194
  ```
176
195
 
177
196
  At the end, the results, a per-protocol summary and the best pick are printed to your normal terminal:
178
197
 
179
198
  ```
180
- ╭─ ★ Best ─────────────────────────────────────────────────╮
181
- │ 🇳🇱 Amsterdam - Tulip over wireguard · 27.0 Mbps · 109 ms │
182
- │ windscribe-cli connect "Tulip" wireguard │
183
- ╰──────────────────────────────────────────────────────────╯
199
+ ╭─ ★ Best (lowest latency) ────────────────────╮
200
+ │ 🇳🇱 Amsterdam - Tulip over wireguard · 109 ms │
201
+ │ windscribe-cli connect "Tulip" wireguard │
202
+ ╰──────────────────────────────────────────────╯
184
203
  ```
185
204
 
186
205
  ## Notes
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
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
- // measures latency and download speed through the tunnel, prints a ranked table.
4
+ // measures latency through the tunnel, prints a ranked table.
5
5
  import { createInterface } from "node:readline";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { parseArgs } from "node:util";
8
- import { CONFIG, DEFAULT_TIMEOUT, forgetLogin, loadConfig, loadDockerWireGuard, MIN_TIMEOUT, saveDockerWireGuard } from "./config.js";
8
+ import { CONFIG, DEFAULT_TIMEOUT, forgetLogin, loadConfig, loadDockerWireGuard, MIN_TIMEOUT, saveDockerWireGuard, SPEED_TEST } from "./config.js";
9
9
  import { notInContainers, stopWorkers } from "./docker.js";
10
10
  import { probe } from "./probe.js";
11
11
  import { Scan } from "./scan.js";
@@ -17,27 +17,28 @@ import { leaveScreen, out } from "./tui/terminal.js";
17
17
  import { cyan, green, yellow } from "./tui/text.js";
18
18
  import { now, sleep } from "./time.js";
19
19
  import { canUpdate, checkUpdate, runUpdate, updateNotice, versionLines } from "./update.js";
20
- import { blockInternet, CLI, connectState, isFirewallOn, PROTOCOLS, restoreFirewall, waitForLogin, windscribe } from "./windscribe.js";
20
+ import { blockInternet, CLI, connectState, isFirewallOn, protocolPorts, PROTOCOLS, restoreFirewall, waitForLogin, windscribe, withPorts } from "./windscribe.js";
21
21
  import { worker } from "./worker.js";
22
22
  const HELP = `Usage: windscout [options]
23
23
 
24
- Find which Windscribe locations and protocols work from your network, ranked by speed.
24
+ Find which Windscribe locations and protocols work from your network, ranked by ${SPEED_TEST ? "speed" : "latency"}.
25
25
  Without -c it opens an interactive country picker (your selection is remembered).
26
26
 
27
27
  Options:
28
28
  -c, --countries LIST comma-separated ISO codes, city names or nicknames (skips the picker)
29
29
  -p, --protocols LIST comma-separated protocols, optional :port (default: ${PROTOCOLS.join(",")})
30
+ -P, --all-ports try each protocol on every port Windscribe offers for it, not just its default
30
31
  -t, --timeout SEC seconds to wait for each connection, at least ${MIN_TIMEOUT} (default: saved, or ${DEFAULT_TIMEOUT})
31
32
  -a, --all also show failed attempts in the final table
32
- -n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
33
- -f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
33
+ ${SPEED_TEST ? " -n, --no-speed connect-only: skip the download test, rank by latency (faster scan)\n" : ""} -f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
34
34
  -r, --random scan the countries in a random order
35
- -d, --docker scan in parallel, in docker containers (as many as docker's CPUs and memory allow, up to 8)
35
+ -d, --docker scan in parallel, in docker containers (one per attempt, up to 16, as docker's CPUs and memory allow)
36
36
  -v, --version show the installed version (and offer to update if a newer one is out)
37
37
  -h, --help show this help
38
38
 
39
39
  Selection is stored in ${CONFIG}
40
- Example: windscout -c DE,NL,TR -p stealth,tcp:443`;
40
+ Examples: windscout -c DE,NL,TR -p stealth,tcp:443
41
+ windscout -c DE -p stealth,tcp -P`;
41
42
  // "DE, NL,,TR" → ["DE", "NL", "TR"]
42
43
  const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
43
44
  const ask = (question) => new Promise((resolve) => {
@@ -83,11 +84,12 @@ async function main() {
83
84
  options: {
84
85
  countries: { type: "string", short: "c" },
85
86
  protocols: { type: "string", short: "p" },
87
+ "all-ports": { type: "boolean", short: "P", default: false },
86
88
  timeout: { type: "string", short: "t" },
87
89
  all: { type: "boolean", short: "a", default: false },
88
90
  help: { type: "boolean", short: "h", default: false },
89
91
  version: { type: "boolean", short: "v", default: false },
90
- "no-speed": { type: "boolean", short: "n", default: false },
92
+ "no-speed": { type: "boolean", short: "n", default: false }, // still accepted while the speed test is hidden
91
93
  firewall: { type: "boolean", short: "f", default: false },
92
94
  random: { type: "boolean", short: "r", default: false },
93
95
  docker: { type: "boolean", short: "d", default: false },
@@ -116,9 +118,10 @@ async function main() {
116
118
  const tty = Boolean(process.stdin.isTTY && out.isTTY);
117
119
  let countries = a.countries ? list(a.countries) : undefined;
118
120
  let protocols = a.protocols ? list(a.protocols) : undefined;
119
- let speedTest = !a["no-speed"];
121
+ let speedTest = SPEED_TEST && !a["no-speed"];
120
122
  let firewall = a.firewall;
121
123
  let random = a.random;
124
+ let allPorts = a["all-ports"];
122
125
  let docker = a.docker;
123
126
  if (!countries) {
124
127
  if (!tty)
@@ -132,6 +135,7 @@ async function main() {
132
135
  // already on in the app: show it on, since the scan won't turn it off
133
136
  firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
134
137
  random: random || (saved?.random ?? false),
138
+ allPorts: allPorts || (saved?.allPorts ?? false),
135
139
  docker: docker || (saved?.docker ?? false),
136
140
  });
137
141
  if (!choice)
@@ -143,11 +147,18 @@ async function main() {
143
147
  speedTest = choice.speed;
144
148
  firewall = choice.firewall;
145
149
  random = choice.random;
150
+ allPorts = choice.allPorts;
146
151
  timeout = choice.timeout;
147
152
  docker = choice.docker;
148
153
  }
149
154
  protocols ??= PROTOCOLS;
150
155
  timeout ??= DEFAULT_TIMEOUT;
156
+ // the picker asked for the ports already: waiting for that keeps windscribe-cli free for the scan
157
+ if (allPorts || !a.countries) {
158
+ const lists = await protocolPorts();
159
+ if (allPorts)
160
+ protocols = withPorts(protocols, lists);
161
+ }
151
162
  if (random)
152
163
  countries = countries.map((c) => [Math.random(), c]).sort(([x], [y]) => x - y).map(([, c]) => c);
153
164
  let probers = [probe], after; // after: what runs here once the containers are done
package/dist/config.js CHANGED
@@ -3,6 +3,8 @@ import { mkdirSync, readFileSync, rmSync, 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
+ // The speed test is hidden: every scan is connect-only, and a saved "on" reads as off. Its code stays; true brings it back
7
+ export const SPEED_TEST = false;
6
8
  export const DEFAULT_TIMEOUT = 15;
7
9
  export const MIN_TIMEOUT = 10; // seconds; shorter cuts off connections that would have worked
8
10
  export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
@@ -14,9 +16,10 @@ export function loadConfig() {
14
16
  return {
15
17
  countries: (c.countries ?? []).filter((x) => COUNTRIES.includes(x)),
16
18
  protocols: (c.protocols ?? []).filter((x) => PROTOCOLS.includes(x)),
17
- speed: c.speed !== false,
19
+ speed: SPEED_TEST && c.speed !== false,
18
20
  firewall: c.firewall === true,
19
21
  random: c.random === true,
22
+ allPorts: c.allPorts === true,
20
23
  timeout: c.timeout >= MIN_TIMEOUT ? c.timeout : DEFAULT_TIMEOUT,
21
24
  docker: c.docker === true,
22
25
  };
package/dist/docker.js CHANGED
@@ -105,12 +105,18 @@ export async function dockerState() {
105
105
  return "ready";
106
106
  }
107
107
  const WORKER_MEMORY = 250e6; // bytes per container: Windscribe's app and helper, a tunnel and node take ~120 MB
108
- const MAX_WORKERS = 8; // ponytail: past this, more containers mostly add load on the one uplink; raise if real scans say so
108
+ // ponytail: past this, more containers mostly add load on the one uplink, and WireGuard keys on the account; raise if real scans say so
109
+ const MAX_WORKERS = 16;
109
110
  /** Protocols the containers can't do, which a parallel scan runs on this computer after them: IKEv2 (Windscribe's Linux app
110
111
  * has none), and WireGuard when their kernel can't run its routing (see DOCKERFILE) */
111
- export const notInContainers = (protocols, wireguard) => new Set(protocols.filter((p) => p === "ikev2" || (p === "wireguard" && !wireguard)));
112
- /** Containers for a scan of this many attempts: one per attempt, within docker's CPUs and 3/4 of its memory */
113
- export const workerCount = (attempts) => Math.max(1, Math.min(attempts, MAX_WORKERS, info.cpus, Math.floor((info.memory * 0.75) / WORKER_MEMORY)));
112
+ export const notInContainers = (protocols, wireguard) => new Set(protocols.filter((p) => /^ikev2(:|$)/.test(p) || (!wireguard && /^wireguard(:|$)/.test(p))));
113
+ /** Containers for this many attempts, when docker can run at most `most`: one per attempt; past that, the fewest that need no
114
+ * more rounds (17 attempts, 16 at most: 9 containers make 2 rounds, as 16 would), since fewer log in sooner and register fewer keys */
115
+ export const containersFor = (attempts, most) => attempts <= most ? Math.max(1, attempts) : Math.ceil(attempts / Math.ceil(attempts / most));
116
+ // ponytail: rough seconds until n containers are logged in: in a test, 1 took 37 s, 16 at once 38-75 s; refine from real runs
117
+ export const loginSeconds = (containers) => 35 + 2.5 * containers;
118
+ /** Containers for a scan of this many attempts, within docker's CPUs and 3/4 of its memory */
119
+ export const workerCount = (attempts) => containersFor(attempts, Math.max(1, Math.min(MAX_WORKERS, info.cpus, Math.floor((info.memory * 0.75) / WORKER_MEMORY))));
114
120
  const DMG = `https://desktop.docker.com/mac/main/${process.arch === "arm64" ? "arm64" : "amd64"}/Docker.dmg`;
115
121
  const START_DESKTOP = process.platform === "win32"
116
122
  ? { label: "start Docker Desktop", cmd: () => `start "" "${DESKTOP}"` }
package/dist/probe.js CHANGED
@@ -27,11 +27,18 @@ export async function probe(query, protocol, { timeout, speedTest, stopped = ()
27
27
  }
28
28
  else if (s.includes("does not exist"))
29
29
  return end("no such location");
30
+ // Windscribe refused this app another WireGuard key for the account: says nothing about this network
31
+ else if (s.includes("WireGuard key limit") && now() - t > 3)
32
+ return end("key limit");
30
33
  else if (s === "Disconnected" && now() - t > 3)
31
34
  return end("failed");
32
35
  onStep(`connecting ${r.location}`.trim());
33
36
  await sleep(1000);
34
37
  }
38
+ // a port the app doesn't offer for the protocol: it connects on the protocol's default instead, so this port wasn't tried
39
+ const port = protocol.split(":")[1], shown = (await windscribe("status")).match(/^Protocol: .*:(\d+)\s*$/m)?.[1];
40
+ if (port && shown && shown !== port)
41
+ return end("no such port");
35
42
  await measure(async () => {
36
43
  onStep(`measuring latency · ${r.location}`);
37
44
  r.latencyMs = await latencyMs();
@@ -1,9 +1,9 @@
1
- import { loadDockerWireGuard, MIN_TIMEOUT, saveConfig } from "../config.js";
2
- import { dockerState, notInContainers, workerCount } from "../docker.js";
1
+ import { loadDockerWireGuard, MIN_TIMEOUT, saveConfig, SPEED_TEST } from "../config.js";
2
+ import { dockerState, loginSeconds, notInContainers, workerCount } from "../docker.js";
3
3
  import { attemptSeconds, estimate } from "../scan.js";
4
4
  import { duration } from "../time.js";
5
5
  import { canUpdate, checkUpdate, versionLines } from "../update.js";
6
- import { COUNTRIES, PROTOCOLS } from "../windscribe.js";
6
+ import { COUNTRIES, protocolPorts, PROTOCOLS } from "../windscribe.js";
7
7
  import { countryName, flag } from "./country.js";
8
8
  import { setupDocker } from "./docker.js";
9
9
  import { draw, enterScreen, input, leaveScreen, out, repoLink } from "./terminal.js";
@@ -11,18 +11,21 @@ import { bold, cyan, dim, green, under, vw, yellow } from "./text.js";
11
11
  import { box, centerIn, grid } from "./widgets.js";
12
12
  const flip = (set, value) => (set.has(value) ? set.delete(value) : set.add(value));
13
13
  /** Resolves to the choice (also saved for next time), null when the user quits, or "update" for u */
14
- export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
14
+ export function pick(saved, { timeout, speedTest, firewall, random, allPorts, docker }) {
15
15
  const selected = new Set(saved?.countries ?? []);
16
16
  const protos = new Set(saved?.protocols.length ? saved.protocols : PROTOCOLS);
17
17
  let cursor = 0, top = 0, message = "", query = "", searching = false;
18
18
  let seconds = String(timeout), editing = false; // the timeout, as typed
19
19
  let dockerAt = null, inDialog = false; // docker's state once checked; a docker dialog has the screen
20
+ let portLists = null; // each protocol's ports, once the app said
20
21
  let hit;
21
22
  const visible = () => {
22
23
  const q = query.toLowerCase();
23
24
  return q ? COUNTRIES.filter((c) => c.toLowerCase().includes(q) || countryName(c).toLowerCase().includes(q)) : COUNTRIES;
24
25
  };
25
- const attempts = () => selected.size * protos.size;
26
+ const ports = (p) => (allPorts && portLists?.get(p)?.length) || 1; // attempts per country for the protocol
27
+ const perCountry = (ps) => [...ps].reduce((n, p) => n + ports(p), 0);
28
+ const attempts = () => selected.size * perCountry(protos);
26
29
  const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
27
30
  enterScreen();
28
31
  return new Promise((resolve) => {
@@ -40,6 +43,12 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
40
43
  top = cursor - height + 1;
41
44
  top = Math.max(0, Math.min(top, matches.length - height));
42
45
  const protoLine = PROTOCOLS.map((p, i) => `${dim(under(String(i + 1)))} ${protos.has(p) ? green("● " + p) : dim("○ " + p)}`).join(" ");
46
+ const listed = portLists && [...portLists.values()].some((l) => l.length), each = perCountry(protos);
47
+ const portsLine = `${dim(under("p"))} ${allPorts ? green("● all ports") : dim("○ all ports")} ${dim(!allPorts
48
+ ? "off: each protocol on its default port"
49
+ : !portLists ? "checking the ports…"
50
+ : !listed ? "this Windscribe app can't list its ports: 2.22 and newer can"
51
+ : `every port of each protocol: ${each} attempt${each === 1 ? "" : "s"} per country${each > protos.size ? `, not ${protos.size}` : ""}`)}`;
43
52
  const speedLine = `${dim(under("s"))} ${speedTest ? green("● speed test") : dim("○ speed test")} ${dim(speedTest
44
53
  ? "ranks by download speed · adds ~15s per working connection"
45
54
  : "off: connect-only, ranks by latency")}`;
@@ -55,8 +64,10 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
55
64
  const timeoutLine = `${dim(under("t"))} timeout ${editing ? bold(seconds) + cyan("▏") : (short ? yellow : bold)(seconds || "?")} s ${dim(`per connection · at least ${MIN_TIMEOUT}`)}`;
56
65
  const n = attempts();
57
66
  const here = PROTOCOLS.filter((p) => docker && protos.has(p) && notInContainers(PROTOCOLS, wireguard).has(p)); // after the containers
58
- const inContainers = selected.size * (protos.size - here.length);
59
- const slots = docker && dockerAt === "ready" && inContainers ? workerCount(inContainers) : 1;
67
+ const inContainers = selected.size * perCountry([...protos].filter((p) => !here.includes(p)));
68
+ const parallel = docker && dockerAt === "ready" && inContainers > 0;
69
+ const slots = parallel ? workerCount(inContainers) : 1;
70
+ const login = parallel ? loginSeconds(slots) : 0; // the containers log in before the scan starts
60
71
  const dockerLabel = "parallel scan in docker containers";
61
72
  const dockerLine = `${dim(under("d"))} ${docker ? green("● " + dockerLabel) : dim("○ " + dockerLabel)} ${dim(!docker
62
73
  ? "off: one attempt at a time"
@@ -82,9 +93,10 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
82
93
  ["quit", under("Q") + "uit"],
83
94
  ];
84
95
  const buttonLine = buttons.map(([id, label]) => (id === "start" ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
85
- const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(...[protoLine, speedLine, fwLine, randomLine, timeoutLine, dockerLine].map(vw)) + 4 });
96
+ const settings = [protoLine, portsLine, ...(SPEED_TEST ? [speedLine] : []), fwLine, randomLine, timeoutLine, dockerLine];
97
+ const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(...settings.map(vw)) + 4 });
86
98
  const width = vw(table[0]);
87
- const head = box([protoLine, speedLine, fwLine, randomLine, timeoutLine, dockerLine], {
99
+ const head = box(settings, {
88
100
  title: bold(cyan("WindScout")),
89
101
  right: `${bold(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
90
102
  width: width - 4,
@@ -96,7 +108,7 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
96
108
  centerIn(buttonLine, width),
97
109
  "",
98
110
  centerIn(n
99
- ? `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(estimate(inContainers, timeout, slots) + estimate(n - inContainers, timeout)))} ${dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""})`)}`
111
+ ? `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(login + estimate(inContainers, timeout, slots) + estimate(n - inContainers, timeout)))} ${dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""}${login ? `, + ~${duration(login)} login` : ""})`)}`
100
112
  : yellow("select at least one country and one protocol"), width),
101
113
  centerIn(dim(editing
102
114
  ? "type the seconds to wait for each connection · enter/esc done"
@@ -104,13 +116,14 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
104
116
  ? "type to filter · ↑↓ move · enter/esc done"
105
117
  // both lines fit 80 columns
106
118
  : `↑↓ move · space select · a/n ${query ? "all/none shown · esc clear" : "all/none · / search"} · enter start · q quit`), width),
107
- centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · s speed test · f firewall · r random · t timeout · d docker`), width),
119
+ centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · p ports${SPEED_TEST ? " · s speed" : ""} · f firewall · r random · t timeout · d docker`), width),
108
120
  ];
109
121
  if (message)
110
122
  lines.push(centerIn(yellow(message), width));
111
123
  lines.push("", ...version.map((l) => centerIn(l, width)), centerIn(dim(repoLink), width));
112
124
  const at = draw(lines);
113
125
  // click map, in screen coordinates
126
+ const row = (line) => at.top + 1 + settings.indexOf(line); // a settings line, under the box's top border
114
127
  let px = at.left + 2; // "│ " before the protocol line
115
128
  const protoHits = PROTOCOLS.map((p, i) => {
116
129
  const len = vw(`${i + 1} ● ${p}`);
@@ -126,7 +139,8 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
126
139
  return range;
127
140
  });
128
141
  hit = {
129
- protoRow: at.top + 1, protoHits, speedRow: at.top + 2, firewallRow: at.top + 3, randomRow: at.top + 4, timeoutRow: at.top + 5, dockerRow: at.top + 6,
142
+ protoRow: row(protoLine), protoHits, portsRow: row(portsLine), speedRow: SPEED_TEST ? row(speedLine) : -1,
143
+ firewallRow: row(fwLine), randomRow: row(randomLine), timeoutRow: row(timeoutLine), dockerRow: row(dockerLine),
130
144
  searchRow: at.top + head.length + 1,
131
145
  firstRow: at.top + head.length + 5, shown: shown.length,
132
146
  buttonRow: at.top + head.length + table.length + 1, buttonHits,
@@ -136,6 +150,7 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
136
150
  checkUpdate().then(() => !closed && render()); // the check usually lands after the first draw
137
151
  if (docker)
138
152
  dockerState().then((s) => ((dockerAt = s), !closed && render()));
153
+ protocolPorts().then((l) => ((portLists = l), !closed && render()));
139
154
  const finish = (value) => {
140
155
  closed = true;
141
156
  out.off("resize", render);
@@ -161,6 +176,7 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
161
176
  speed: speedTest,
162
177
  firewall,
163
178
  random,
179
+ allPorts,
164
180
  timeout,
165
181
  docker,
166
182
  };
@@ -214,6 +230,8 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
214
230
  if (i >= 0)
215
231
  flip(protos, PROTOCOLS[i]);
216
232
  }
233
+ else if (y === hit.portsRow)
234
+ allPorts = !allPorts;
217
235
  else if (y === hit.speedRow)
218
236
  speedTest = !speedTest;
219
237
  else if (y === hit.firewallRow)
@@ -294,7 +312,9 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
294
312
  matches.forEach((c) => selected.add(c));
295
313
  else if (k === "n")
296
314
  matches.forEach((c) => selected.delete(c));
297
- else if (k === "s")
315
+ else if (k === "p")
316
+ allPorts = !allPorts;
317
+ else if (k === "s" && SPEED_TEST)
298
318
  speedTest = !speedTest;
299
319
  else if (k === "f")
300
320
  firewall = !firewall;
@@ -24,7 +24,9 @@ export function showProgress(scan, blocking) {
24
24
  const eta = (total - done) * (done ? elapsed / done : attemptSeconds(timeout) / slots.length);
25
25
  const spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"[frame++ % 10];
26
26
  const ok = sortOk(results);
27
- const failed = finished.filter((c) => !results.some((r) => r.query === c && r.status === "ok"));
27
+ // blocked: nothing worked, and something was really tried (a key limit or a port the app doesn't offer tries nothing)
28
+ const failed = finished.filter((c) => !results.some((r) => r.query === c && r.status === "ok")
29
+ && results.some((r) => r.query === c && r.status !== "key limit" && r.status !== "no such port"));
28
30
  const attempt = ({ current: c }) => (c ? `${flag(c.country)} ${countryName(c.country)} ${dim("·")} ${c.protocol}` : "");
29
31
  const many = slots.length > 1; // a parallel scan: a line per container
30
32
  const pct = bold(`${String(Math.floor(fraction * 100)).padStart(3)}%`);
@@ -30,6 +30,15 @@ export function report(results, protocols, { all, speedTest }) {
30
30
  console.log();
31
31
  if (avail.length)
32
32
  console.log(avail.join("\n"));
33
+ const limited = results.filter((r) => r.status === "key limit");
34
+ if (limited.length) {
35
+ const many = limited.length > 1, list = (xs) => [...new Set(xs)].join(",");
36
+ console.log([
37
+ yellow(`⚠ ${limited.length} WireGuard attempt${many ? "s" : ""} hit your Windscribe account's limit of WireGuard keys`),
38
+ dim(` (each docker container registers a key of its own), so ${many ? "these don't" : "it doesn't"} show whether WireGuard works from here.`),
39
+ ` To find out, scan ${many ? "them" : "it"} on this computer: ${cyan(`windscout -c "${list(limited.map((r) => r.query))}" -p ${list(limited.map((r) => r.protocol))}`)}`,
40
+ ].join("\n"));
41
+ }
33
42
  if (!ok.length) {
34
43
  console.log(box([yellow("Nothing connected."),
35
44
  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"));
@@ -37,7 +46,7 @@ export function report(results, protocols, { all, speedTest }) {
37
46
  }
38
47
  console.log(main.join("\n"));
39
48
  const stats = protocols.map((p) => {
40
- const tried = results.filter((r) => r.protocol === p && r.status !== "no such location");
49
+ const tried = results.filter((r) => r.protocol === p && !["no such location", "key limit", "no such port"].includes(r.status));
41
50
  const good = tried.filter((r) => r.status === "ok");
42
51
  return { p, good: good.length, tried: tried.length, best: sortOk(good).at(0) };
43
52
  }).sort((x, y) => y.good - x.good || (y.best?.speedMbps ?? 0) - (x.best?.speedMbps ?? 0) || (x.best?.latencyMs ?? 1e9) - (y.best?.latencyMs ?? 1e9));
@@ -1,5 +1,5 @@
1
1
  import { countryName, flag } from "./country.js";
2
- import { bold, dim, fmt, green, red } from "./text.js";
2
+ import { bold, dim, fmt, green, red, yellow } from "./text.js";
3
3
  import { grid } from "./widgets.js";
4
4
  const RESULT_COLS = [
5
5
  { title: "#", align: "right" }, { title: "Country" }, { title: "Location" }, { title: "Protocol" },
@@ -14,15 +14,27 @@ const resultRow = (r, i) => [
14
14
  r.connectS === null ? "-" : `${fmt(r.connectS, 1)} s`,
15
15
  r.latencyMs === null ? "-" : `${fmt(r.latencyMs, 0)} ms`,
16
16
  r.speedMbps === null ? "-" : bold(`${fmt(r.speedMbps, 1)} Mbps`),
17
- r.status === "ok" ? green("✓ ok") : red(r.status),
17
+ r.status === "ok" ? green("✓ ok") : r.status === "key limit" || r.status === "no such port" ? yellow(r.status) : red(r.status),
18
18
  ];
19
19
  /** Numbered attempts, in the given order; the Speed column only when the speed test ran */
20
20
  export function resultTable(results, speedTest, options) {
21
21
  const keep = (_, i) => speedTest || i !== SPEED_COL;
22
22
  return grid(RESULT_COLS.filter(keep), results.map((r, i) => resultRow(r, i).filter(keep)), options);
23
23
  }
24
- /** One row per country, ✓/✗ per protocol (· = not tried yet); no lines until something was tried */
24
+ const base = (protocol) => protocol.split(":")[0];
25
+ /** A protocol's cell: ✓/✗, with how many of its ports worked when it has several (✓ 3/11); · = not tried yet,
26
+ * key limit = couldn't try WireGuard, no port = the app doesn't offer it. Those last two don't count */
27
+ function cell(rs, ports) {
28
+ const tried = rs.filter((r) => ports.includes(r.protocol));
29
+ const real = tried.filter((r) => r.status !== "key limit" && r.status !== "no such port");
30
+ const good = real.filter((r) => r.status === "ok").length, count = ports.length > 1 ? ` ${good}/${real.length}` : "";
31
+ if (real.length)
32
+ return good ? green(`✓${count}`) : red(`✗${count}`);
33
+ return tried.some((r) => r.status === "key limit") ? yellow("key limit") : tried.length ? yellow("no port") : dim("·");
34
+ }
35
+ /** One row per country, a cell per protocol (its ports share it); no lines until something was tried */
25
36
  export function matrix(results, protocols, { minWidth = 0, limit = Infinity } = {}) {
37
+ const columns = [...new Set(protocols.map(base))].map((b) => protocols.filter((p) => base(p) === b));
26
38
  const byCountry = new Map();
27
39
  for (const r of results) {
28
40
  if (r.status === "no such location" || r.status === "interrupted")
@@ -45,15 +57,13 @@ export function matrix(results, protocols, { minWidth = 0, limit = Infinity } =
45
57
  [
46
58
  `${flag(q)} ${countryName(q)}`,
47
59
  first,
48
- ...protocols.map((p) => {
49
- const r = rs.find((x) => x.protocol === p);
50
- return !r ? dim("·") : r.status === "ok" ? green("✓") : red("✗");
51
- }),
60
+ ...columns.map((ports) => cell(rs, ports)),
52
61
  ],
53
- ...more.map((place) => ["", place, ...protocols.map(() => "")]),
62
+ ...more.map((place) => ["", place, ...columns.map(() => "")]),
54
63
  ];
55
64
  }).slice(0, limit);
56
- return grid([{ title: "Country" }, { title: "Location(s)" }, ...protocols.map((p) => ({ title: p, align: "center" }))], rows, {
65
+ return grid([{ title: "Country" }, { title: "Location(s)" },
66
+ ...columns.map((ports) => ({ title: ports.length > 1 ? base(ports[0]) : ports[0], align: "center" }))], rows, {
57
67
  caption: bold("Availability"),
58
68
  captionRight: dim(`${reachable}/${ranked.length} countries reachable${ranked.length > limit ? ` · top ${limit}` : ""}`),
59
69
  minWidth,
@@ -15,6 +15,19 @@ const DESKTOP_CLI = {
15
15
  let cliBin = CLI;
16
16
  // IKEv2 is offered by the macOS/Windows apps only
17
17
  export const PROTOCOLS = ["wireguard", ...(process.platform === "linux" ? [] : ["ikev2"]), "stealth", "wstunnel", "udp", "tcp"];
18
+ let portLists;
19
+ /** The ports the app offers for each protocol, its default first; [] where it lists none (IKEv2 on Linux, or apps before 2.22,
20
+ * which have no `ports`). Asked once, one protocol at a time: windscribe-cli runs one at a time */
21
+ export const protocolPorts = () => (portLists ??= (async () => {
22
+ const lists = new Map();
23
+ for (const p of PROTOCOLS) {
24
+ const out = await windscribe("ports", p).catch(() => ""); // can't ask: no list, so the default port
25
+ lists.set(p, out.match(/^\d+(, \d+)*$/m)?.[0].split(", ").map(Number) ?? []);
26
+ }
27
+ return lists;
28
+ })());
29
+ /** Each protocol given without a port, on every port in lists: "tcp" → "tcp:443", "tcp:587", …; the rest as given */
30
+ export const withPorts = (protocols, lists) => protocols.flatMap((p) => (!p.includes(":") && lists.get(p)?.length ? lists.get(p).map((port) => `${p}:${port}`) : [p]));
18
31
  // ponytail: hardcoded because `windscribe-cli locations` returns nothing on 2.24; refresh when Windscribe adds countries
19
32
  export const COUNTRIES = `US CA MX BR AR CL CO PE GB IE FR DE NL BE LU CH AT IT ES PT DK NO SE FI IS
20
33
  PL CZ SK HU RO BG GR CY TR RS HR SI BA MK AL MD UA EE LV LT IL AE AZ GE AM KZ IN JP KR
package/dist/worker.js CHANGED
@@ -41,9 +41,10 @@ export async function worker() {
41
41
  writeFileSync(SETTINGS, withSession(ini, session));
42
42
  // no systemd in a container: start the app like its unit would, as this user (its CLI refuses root)
43
43
  spawn(APP, { stdio: "ignore" }).on("error", () => { }); // not there: its CLI never answers below
44
- // its CLI answers once it's up, and says logged in once Windscribe accepts the session, or the error it got instead
44
+ // its CLI answers once it's up, and says logged in once Windscribe accepts the session, or the error it got instead.
45
+ // One container took 37 s in a test, 16 at once 38-75 s
45
46
  let status = "";
46
- for (const t = now(); now() - t < 120 && !/Login state: (Logged in|Error)/.test(status = await windscribe("status"));)
47
+ for (const t = now(); now() - t < 180 && !/Login state: (Logged in|Error)/.test(status = await windscribe("status"));)
47
48
  await sleep(1000);
48
49
  if (!status.includes("Logged in")) {
49
50
  const state = status.match(/^Login state: (.*)$/m)?.[1].trim();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "windscout",
3
- "version": "0.6.1",
4
- "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
3
+ "version": "0.6.4",
4
+ "description": "Find which Windscribe locations and protocols work from your network, ranked by latency (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "windscout": "dist/cli.js"
@@ -25,14 +25,12 @@
25
25
  "keywords": [
26
26
  "windscribe",
27
27
  "vpn",
28
- "speedtest",
29
28
  "wireguard",
30
29
  "stealth",
31
30
  "cli",
32
31
  "censorship",
33
32
  "firewall",
34
33
  "unblock",
35
- "vpn-speed-test",
36
34
  "server-finder",
37
35
  "docker"
38
36
  ],