windscout 0.5.0 → 0.5.1

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
@@ -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
@@ -3,7 +3,7 @@
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
5
  import { parseArgs } from "node:util";
6
- import { CONFIG, loadConfig } from "./config.js";
6
+ import { CONFIG, DEFAULT_TIMEOUT, loadConfig, MIN_TIMEOUT } from "./config.js";
7
7
  import { Scan } from "./scan.js";
8
8
  import { pick } from "./tui/picker.js";
9
9
  import { logProgress, showProgress } from "./tui/progress.js";
@@ -20,7 +20,7 @@ Without -c it opens an interactive country picker (your selection is remembered)
20
20
  Options:
21
21
  -c, --countries LIST comma-separated ISO codes, city names or nicknames (skips the picker)
22
22
  -p, --protocols LIST comma-separated protocols, optional :port (default: ${PROTOCOLS.join(",")})
23
- -t, --timeout SEC seconds to wait for each connection (default: 20)
23
+ -t, --timeout SEC seconds to wait for each connection, at least ${MIN_TIMEOUT} (default: saved, or ${DEFAULT_TIMEOUT})
24
24
  -a, --all also show failed attempts in the final table
25
25
  -n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
26
26
  -f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
@@ -36,7 +36,7 @@ async function main() {
36
36
  options: {
37
37
  countries: { type: "string", short: "c" },
38
38
  protocols: { type: "string", short: "p" },
39
- timeout: { type: "string", short: "t", default: "20" },
39
+ timeout: { type: "string", short: "t" },
40
40
  all: { type: "boolean", short: "a", default: false },
41
41
  help: { type: "boolean", short: "h", default: false },
42
42
  version: { type: "boolean", short: "v", default: false },
@@ -55,9 +55,9 @@ async function main() {
55
55
  console.error(yellow(notice));
56
56
  return;
57
57
  }
58
- const timeout = Number(a.timeout);
59
- if (!(timeout > 0))
60
- throw new Error("--timeout must be a positive number");
58
+ let timeout = a.timeout ? Number(a.timeout) : undefined;
59
+ if (timeout !== undefined && !(timeout >= MIN_TIMEOUT))
60
+ throw new Error(`--timeout must be at least ${MIN_TIMEOUT} seconds`);
61
61
  const status = await waitForLogin();
62
62
  const tty = Boolean(process.stdin.isTTY && out.isTTY);
63
63
  let countries = a.countries ? list(a.countries) : undefined;
@@ -71,7 +71,7 @@ async function main() {
71
71
  if (protocols && saved)
72
72
  saved.protocols = protocols.filter((p) => PROTOCOLS.includes(p));
73
73
  const choice = await pick(saved, {
74
- timeout,
74
+ timeout: timeout ?? saved?.timeout ?? DEFAULT_TIMEOUT,
75
75
  speedTest: speedTest && (saved?.speed ?? false), // first run: connect-only
76
76
  // already on in the app: show it on, since the scan won't turn it off
77
77
  firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
@@ -82,8 +82,10 @@ async function main() {
82
82
  protocols ??= choice.protocols;
83
83
  speedTest = choice.speed;
84
84
  firewall = choice.firewall;
85
+ timeout = choice.timeout;
85
86
  }
86
87
  protocols ??= PROTOCOLS;
88
+ timeout ??= DEFAULT_TIMEOUT;
87
89
  const scan = new Scan(countries, protocols, { timeout, speedTest });
88
90
  const blocking = firewall && (await blockInternet());
89
91
  const view = tty ? showProgress(scan, blocking) : logProgress(scan);
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 {
@@ -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 - 20 - (notice ? 1 : 0));
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(searching
80
- ? "type to filter · ↑↓ move · enter/esc done"
81
- : `↑↓ move · space select · a/n ${query ? "all/none shown" : "all/none"} · / search`), width),
82
- centerIn(dim(searching ? "" : `1-${PROTOCOLS.length} protocols · s speed test · f firewall · enter start · ${query ? "esc clear · " : ""}q quit`), width),
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windscout",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {