windscout 0.5.6 → 0.5.8

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
@@ -20,7 +20,7 @@ WindScout drives the official `windscribe-cli`: it connects to each country × p
20
20
  npm install -g windscout # or run once with: npx windscout
21
21
  ```
22
22
 
23
- On startup, WindScout checks npm for a newer version. If there is one, it shows the command to update, both in the picker and after the final report.
23
+ On startup, WindScout checks npm for a newer version. Above the repository link at the bottom of the screen, it shows the installed version in grey (`windscout 0.5.6 · latest` once npm confirms nothing newer is out), or, when a newer one is out, an update notice in yellow. In the picker, press `u` to update and restart on the new version. The update runs `npm install -g windscout@latest`, with `sudo` when your global npm folder belongs to root (sudo then asks for your password). The update command is also printed after the final report.
24
24
 
25
25
  ## Usage
26
26
 
@@ -52,6 +52,8 @@ Run `windscout` with no arguments to open the interactive picker:
52
52
  | `1`–`6` | toggle a protocol (`1`–`5` on Linux, which has no IKEv2) |
53
53
  | `s` | toggle the speed test (off = connect-only mode) |
54
54
  | `f` | toggle "block internet while scanning" (firewall) |
55
+ | `r` | toggle random order: scan the selected countries shuffled, a new order each run |
56
+ | `u` | update to the newer version and restart (only when the picker shows one is out) |
55
57
  | `enter` | start scanning |
56
58
  | `q` | quit (during a scan: stop and show partial results; press again to force quit) |
57
59
 
@@ -79,7 +81,8 @@ windscout -t 40 -a # longer connect timeout, also list
79
81
  | `-a, --all` | include failed attempts in the final table | off |
80
82
  | `-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
83
  | `-f, --firewall` | block internet while scanning, except through the VPN | picker: saved, or off; with `-c`: off |
82
- | `-v, --version` | print the installed version; if a newer one is on npm, show the update command and ask (y/n) to run it | |
84
+ | `-r, --random` | scan the countries in a random order | picker: saved, or off; with `-c`: off |
85
+ | `-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 | |
83
86
 
84
87
  ## Connect-only mode
85
88
 
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { command, report, target } from "./tui/report.js";
13
13
  import { leaveScreen, out } from "./tui/terminal.js";
14
14
  import { cyan, green, yellow } from "./tui/text.js";
15
15
  import { now, sleep } from "./time.js";
16
- import { checkUpdate, UPDATE_COMMAND, updateNotice, VERSION, viaNpx } from "./update.js";
16
+ import { canUpdate, checkUpdate, runUpdate, updateNotice, versionLines } from "./update.js";
17
17
  import { blockInternet, CLI, connectState, isFirewallOn, PROTOCOLS, restoreFirewall, waitForLogin, windscribe } from "./windscribe.js";
18
18
  const HELP = `Usage: windscout [options]
19
19
 
@@ -27,6 +27,7 @@ Options:
27
27
  -a, --all also show failed attempts in the final table
28
28
  -n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
29
29
  -f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
30
+ -r, --random scan the countries in a random order
30
31
  -v, --version show the installed version (and offer to update if a newer one is out)
31
32
  -h, --help show this help
32
33
 
@@ -61,6 +62,17 @@ async function offerConnect(choices, timeout) {
61
62
  return;
62
63
  }
63
64
  }
65
+ /** u in the picker: update, then run the new version in this one's place, with the same arguments */
66
+ function updateAndRestart() {
67
+ const status = runUpdate();
68
+ if (status) {
69
+ console.error(yellow(`update failed (exit ${status})`));
70
+ process.exitCode = status;
71
+ return;
72
+ }
73
+ console.log(green("updated · restarting windscout"));
74
+ process.exitCode = spawnSync(process.execPath, [...process.execArgv, ...process.argv.slice(1)], { stdio: "inherit" }).status ?? 1;
75
+ }
64
76
  async function main() {
65
77
  const { values: a } = parseArgs({
66
78
  options: {
@@ -72,25 +84,17 @@ async function main() {
72
84
  version: { type: "boolean", short: "v", default: false },
73
85
  "no-speed": { type: "boolean", short: "n", default: false },
74
86
  firewall: { type: "boolean", short: "f", default: false },
87
+ random: { type: "boolean", short: "r", default: false },
75
88
  },
76
89
  });
77
90
  if (a.help)
78
91
  return console.log(HELP);
79
92
  checkUpdate(); // runs while the app starts and the user picks, before the VPN starts switching
80
93
  if (a.version) {
81
- console.log(`windscout ${VERSION}`);
82
94
  await checkUpdate();
83
- const notice = updateNotice();
84
- if (!notice)
85
- return;
86
- console.error(yellow(notice));
87
- if (process.stdin.isTTY && out.isTTY && !viaNpx() && /^y(es)?$/i.test(await ask("update now? (y/n) "))) {
88
- console.log(cyan(UPDATE_COMMAND));
89
- // shell: npm is npm.cmd on Windows
90
- const { status } = spawnSync(UPDATE_COMMAND, { shell: true, stdio: "inherit" });
91
- if (status)
92
- process.exitCode = status;
93
- }
95
+ console.log(versionLines().join("\n"));
96
+ if (canUpdate() && process.stdin.isTTY && out.isTTY && /^y(es)?$/i.test(await ask("update now? (y/n) ")))
97
+ process.exitCode = runUpdate();
94
98
  return;
95
99
  }
96
100
  let timeout = a.timeout ? Number(a.timeout) : undefined;
@@ -102,6 +106,7 @@ async function main() {
102
106
  let protocols = a.protocols ? list(a.protocols) : undefined;
103
107
  let speedTest = !a["no-speed"];
104
108
  let firewall = a.firewall;
109
+ let random = a.random;
105
110
  if (!countries) {
106
111
  if (!tty)
107
112
  throw new Error("no terminal for the picker: pass countries with -c");
@@ -113,17 +118,23 @@ async function main() {
113
118
  speedTest: speedTest && (saved?.speed ?? false), // first run: connect-only
114
119
  // already on in the app: show it on, since the scan won't turn it off
115
120
  firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
121
+ random: random || (saved?.random ?? false),
116
122
  });
117
123
  if (!choice)
118
124
  return;
125
+ if (choice === "update")
126
+ return updateAndRestart();
119
127
  countries = choice.countries;
120
128
  protocols ??= choice.protocols;
121
129
  speedTest = choice.speed;
122
130
  firewall = choice.firewall;
131
+ random = choice.random;
123
132
  timeout = choice.timeout;
124
133
  }
125
134
  protocols ??= PROTOCOLS;
126
135
  timeout ??= DEFAULT_TIMEOUT;
136
+ if (random)
137
+ countries = countries.map((c) => [Math.random(), c]).sort(([x], [y]) => x - y).map(([, c]) => c);
127
138
  const scan = new Scan(countries, protocols, { timeout, speedTest });
128
139
  const blocking = firewall && (await blockInternet());
129
140
  const view = tty ? showProgress(scan, blocking) : logProgress(scan);
@@ -134,9 +145,8 @@ async function main() {
134
145
  console.log(yellow("interrupted, showing partial results"));
135
146
  const choices = report(scan.results, protocols, { all: a.all, speedTest });
136
147
  await checkUpdate();
137
- const notice = updateNotice();
138
- if (notice)
139
- console.error("\n" + yellow(notice));
148
+ if (updateNotice())
149
+ console.error("\n" + versionLines().join("\n"));
140
150
  if (tty && choices.length)
141
151
  await offerConnect(choices, timeout);
142
152
  }
package/dist/config.js CHANGED
@@ -15,6 +15,7 @@ export function loadConfig() {
15
15
  protocols: (c.protocols ?? []).filter((x) => PROTOCOLS.includes(x)),
16
16
  speed: c.speed !== false,
17
17
  firewall: c.firewall === true,
18
+ random: c.random === true,
18
19
  timeout: c.timeout >= MIN_TIMEOUT ? c.timeout : DEFAULT_TIMEOUT,
19
20
  };
20
21
  }
@@ -1,15 +1,15 @@
1
1
  import { MIN_TIMEOUT, saveConfig } from "../config.js";
2
2
  import { attemptSeconds, estimate } from "../scan.js";
3
3
  import { duration } from "../time.js";
4
- import { checkUpdate, updateNotice } from "../update.js";
4
+ import { canUpdate, checkUpdate, versionLines } from "../update.js";
5
5
  import { COUNTRIES, PROTOCOLS } from "../windscribe.js";
6
6
  import { countryName, flag } from "./country.js";
7
7
  import { draw, enterScreen, input, leaveScreen, out, repoLink } from "./terminal.js";
8
8
  import { bold, cyan, dim, green, under, vw, yellow } from "./text.js";
9
9
  import { box, centerIn, grid } from "./widgets.js";
10
10
  const flip = (set, value) => (set.has(value) ? set.delete(value) : set.add(value));
11
- /** Resolves to the choice (also saved for next time), or null when the user quits */
12
- export function pick(saved, { timeout, speedTest, firewall }) {
11
+ /** Resolves to the choice (also saved for next time), null when the user quits, or "update" for u */
12
+ export function pick(saved, { timeout, speedTest, firewall, random }) {
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;
@@ -23,9 +23,9 @@ export function pick(saved, { timeout, speedTest, firewall }) {
23
23
  return new Promise((resolve) => {
24
24
  const render = () => {
25
25
  const rows = out.rows || 24;
26
- const notice = updateNotice();
26
+ const version = versionLines(true);
27
27
  const matches = visible();
28
- const height = Math.max(3, rows - 21 - (notice ? 1 : 0));
28
+ const height = Math.max(3, rows - 22 - version.length);
29
29
  cursor = Math.max(0, Math.min(cursor, matches.length - 1));
30
30
  if (cursor < top)
31
31
  top = cursor;
@@ -40,6 +40,9 @@ export function pick(saved, { timeout, speedTest, firewall }) {
40
40
  const fwLine = `${dim(under("f"))} ${firewall ? green("● " + fwLabel) : dim("○ " + fwLabel)} ${dim(firewall
41
41
  ? "only VPN traffic gets through"
42
42
  : "off: apps may leak in between")}`;
43
+ const randomLine = `${dim(under("r"))} ${random ? green("● random order") : dim("○ random order")} ${dim(random
44
+ ? "scans the countries shuffled, a new order each run"
45
+ : "off: scans in list order")}`;
43
46
  timeout = Number(seconds);
44
47
  const short = timeout < MIN_TIMEOUT;
45
48
  const timeoutLine = `${dim(under("t"))} timeout ${editing ? bold(seconds) + cyan("▏") : (short ? yellow : bold)(seconds || "?")} s ${dim(`per connection · at least ${MIN_TIMEOUT}`)}`;
@@ -63,9 +66,9 @@ export function pick(saved, { timeout, speedTest, firewall }) {
63
66
  ["quit", under("Q") + "uit"],
64
67
  ];
65
68
  const buttonLine = buttons.map(([id, label]) => (id === "start" ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
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 });
69
+ const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine), vw(fwLine), vw(randomLine), vw(timeoutLine)) + 4 });
67
70
  const width = vw(table[0]);
68
- const head = box([protoLine, speedLine, fwLine, timeoutLine], {
71
+ const head = box([protoLine, speedLine, fwLine, randomLine, timeoutLine], {
69
72
  title: bold(cyan("WindScout")),
70
73
  right: `${bold(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
71
74
  width: width - 4,
@@ -85,13 +88,11 @@ export function pick(saved, { timeout, speedTest, firewall }) {
85
88
  : searching
86
89
  ? "type to filter · ↑↓ move · enter/esc done"
87
90
  : `↑↓ 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),
91
+ centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · s speed test · f firewall · r random · t timeout · enter start · ${query ? "esc clear · " : ""}q quit`), width),
89
92
  ];
90
93
  if (message)
91
94
  lines.push(centerIn(yellow(message), width));
92
- lines.push("", centerIn(dim(repoLink), width));
93
- if (notice)
94
- lines.push(centerIn(yellow(notice), width));
95
+ lines.push("", ...version.map((l) => centerIn(l, width)), centerIn(dim(repoLink), width));
95
96
  const at = draw(lines);
96
97
  // click map, in screen coordinates
97
98
  let px = at.left + 2; // "│ " before the protocol line
@@ -109,20 +110,20 @@ export function pick(saved, { timeout, speedTest, firewall }) {
109
110
  return range;
110
111
  });
111
112
  hit = {
112
- protoRow: at.top + 1, protoHits, speedRow: at.top + 2, firewallRow: at.top + 3, timeoutRow: at.top + 4,
113
+ protoRow: at.top + 1, protoHits, speedRow: at.top + 2, firewallRow: at.top + 3, randomRow: at.top + 4, timeoutRow: at.top + 5,
113
114
  searchRow: at.top + head.length + 1,
114
115
  firstRow: at.top + head.length + 5, shown: shown.length,
115
116
  buttonRow: at.top + head.length + table.length + 1, buttonHits,
116
117
  };
117
118
  };
118
119
  let closed = false;
119
- checkUpdate().then(() => updateNotice() && !closed && render()); // the check usually lands after the first draw
120
+ checkUpdate().then(() => !closed && render()); // the check usually lands after the first draw
120
121
  const finish = (value) => {
121
122
  closed = true;
122
123
  out.off("resize", render);
123
124
  input.removeAllListeners("keypress");
124
125
  input.removeAllListeners("mouse");
125
- if (!value)
126
+ if (!value || value === "update")
126
127
  leaveScreen();
127
128
  resolve(value);
128
129
  };
@@ -141,6 +142,7 @@ export function pick(saved, { timeout, speedTest, firewall }) {
141
142
  protocols: PROTOCOLS.filter((p) => protos.has(p)),
142
143
  speed: speedTest,
143
144
  firewall,
145
+ random,
144
146
  timeout,
145
147
  };
146
148
  saveConfig(choice);
@@ -180,6 +182,8 @@ export function pick(saved, { timeout, speedTest, firewall }) {
180
182
  speedTest = !speedTest;
181
183
  else if (y === hit.firewallRow)
182
184
  firewall = !firewall;
185
+ else if (y === hit.randomRow)
186
+ random = !random;
183
187
  else if (y === hit.buttonRow) {
184
188
  const id = hit.buttonHits.find(([, a, b]) => x >= a && x < b)?.[0];
185
189
  if (id === "start" && start())
@@ -255,8 +259,12 @@ export function pick(saved, { timeout, speedTest, firewall }) {
255
259
  speedTest = !speedTest;
256
260
  else if (k === "f")
257
261
  firewall = !firewall;
262
+ else if (k === "r")
263
+ random = !random;
258
264
  else if (k === "t")
259
265
  editing = true, seconds = "";
266
+ else if (k === "u" && canUpdate())
267
+ return finish("update");
260
268
  else if (/^[1-9]$/.test(str ?? "") && PROTOCOLS[Number(str) - 1])
261
269
  flip(protos, PROTOCOLS[Number(str) - 1]);
262
270
  else if (k === "escape" && query)
@@ -1,6 +1,7 @@
1
1
  import { sortOk } from "../probe.js";
2
2
  import { attemptSeconds } from "../scan.js";
3
3
  import { duration, now } from "../time.js";
4
+ import { versionLines } from "../update.js";
4
5
  import { CLI, restoreFirewall } from "../windscribe.js";
5
6
  import { countryName, flag } from "./country.js";
6
7
  import { matrix, resultTable } from "./tables.js";
@@ -34,7 +35,8 @@ export function showProgress(scan, blocking) {
34
35
  ].join(dim(" │ "));
35
36
  const minW = Math.max(64, vw(stats) + 6); // panel and table share one width
36
37
  // results table gets whatever height the status panel and footer leave
37
- const room = Math.max(1, rows - 22);
38
+ const version = versionLines();
39
+ const room = Math.max(1, rows - 22 - version.length);
38
40
  const table = view === "availability"
39
41
  ? matrix(results, protocols, { minWidth: minW, limit: room })
40
42
  : ok.length
@@ -73,7 +75,7 @@ export function showProgress(scan, blocking) {
73
75
  txt = txt.slice(0, -8) + "…"; // ponytail: crude trim, fine for a status line
74
76
  lines.push("", centerIn(txt, width));
75
77
  }
76
- lines.push("", centerIn(dim(`tab ${view === "ranking" ? "availability" : "ranking"} view · p ${paused ? "resume" : "pause"} · q stop · q twice force quit`), width), "", centerIn(dim(repoLink), width));
78
+ lines.push("", centerIn(dim(`tab ${view === "ranking" ? "availability" : "ranking"} view · p ${paused ? "resume" : "pause"} · q stop · q twice force quit`), width), "", ...version.map((l) => centerIn(l, width)), centerIn(dim(repoLink), width));
77
79
  draw(lines);
78
80
  };
79
81
  enterScreen();
package/dist/update.js CHANGED
@@ -1,7 +1,10 @@
1
- // The installed version, and whether npm has a newer one.
2
- import { readFileSync } from "node:fs";
1
+ // The installed version, whether npm has a newer one, and updating to it.
2
+ import { spawnSync } from "node:child_process";
3
+ import { accessSync, constants, readFileSync } from "node:fs";
4
+ import { cyan, dim, under, yellow } from "./tui/text.js";
3
5
  export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
4
6
  let latest = null; // newer version on npm, once known
7
+ let upToDate = false; // npm answered, and has nothing newer
5
8
  let check;
6
9
  // numeric x.y.z compare; parseInt drops a prerelease suffix ("0-beta" → 0)
7
10
  export function newer(a, b) {
@@ -15,17 +18,48 @@ export function newer(a, b) {
15
18
  export function checkUpdate() {
16
19
  check ??= fetch("https://registry.npmjs.org/windscout/latest", { signal: AbortSignal.timeout(3000) })
17
20
  .then((r) => r.json())
18
- .then(({ version }) => { if (version && newer(version, VERSION))
19
- latest = version; })
21
+ .then(({ version }) => {
22
+ if (version && newer(version, VERSION))
23
+ latest = version;
24
+ else
25
+ upToDate = Boolean(version);
26
+ })
20
27
  .catch(() => { });
21
28
  return check;
22
29
  }
23
30
  // run through npx: there's nothing installed to update, the next npx windscout@latest fetches it
24
31
  export const viaNpx = () => process.env.npm_command === "exec";
25
- export const UPDATE_COMMAND = "npm install -g windscout@latest";
26
- /** "update available: …" with the command to run, once checkUpdate() found a newer version */
27
- export function updateNotice() {
32
+ // npm replaces the folder this package sits in; a root-owned global folder (system Node) needs sudo for that
33
+ const sudo = (() => {
34
+ try {
35
+ accessSync(new URL("../..", import.meta.url), constants.W_OK);
36
+ return false;
37
+ }
38
+ catch {
39
+ return true;
40
+ }
41
+ })();
42
+ const INSTALL = "npm install -g windscout@latest";
43
+ export const UPDATE_COMMAND = (sudo ? "sudo " : "") + INSTALL;
44
+ // npm is Node too: through a slow tunnel its downloads time out like fetch did (see measure.ts) without these
45
+ const NODE_FIX = "--no-network-family-autoselection --dns-result-order=ipv4first";
46
+ /** A newer version is out, and npm can install it (under npx there's nothing installed to update) */
47
+ export const canUpdate = () => latest !== null && !viaNpx();
48
+ /** Runs the update in this terminal (sudo asks for the password here); returns npm's exit status */
49
+ export function runUpdate() {
50
+ console.log(cyan(UPDATE_COMMAND));
51
+ const NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ""} ${NODE_FIX}`.trim();
52
+ // sudo clears the environment, so env sets NODE_OPTIONS on its side; shell: npm is npm.cmd on Windows
53
+ return spawnSync(sudo ? `sudo env NODE_OPTIONS="${NODE_FIX}" ${INSTALL}` : INSTALL, { shell: true, stdio: "inherit", env: { ...process.env, NODE_OPTIONS } }).status ?? 1;
54
+ }
55
+ /** "update available: …" and how to get it, once checkUpdate() found a newer version; onKey: this screen updates on u */
56
+ export function updateNotice(onKey = false) {
28
57
  if (!latest)
29
58
  return null;
30
- return `update available: ${VERSION} → ${latest} · run: ${viaNpx() ? "npx windscout@latest" : UPDATE_COMMAND}`;
59
+ return [`update available: ${VERSION} → ${latest}`,
60
+ viaNpx() ? "quit, then run: npx windscout@latest"
61
+ : onKey ? `press ${under("u")} to update and restart · runs ${UPDATE_COMMAND}`
62
+ : `run: ${UPDATE_COMMAND}, then restart windscout`];
31
63
  }
64
+ /** The version line(s) above the repo link: the update notice in yellow, or the installed version in grey */
65
+ export const versionLines = (onKey = false) => updateNotice(onKey)?.map(yellow) ?? [dim(`windscout ${VERSION}${upToDate ? " · latest" : ""}`)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windscout",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {