windscout 0.5.6 → 0.5.7

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,7 @@ 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
+ | `u` | update to the newer version and restart (only when the picker shows one is out) |
55
56
  | `enter` | start scanning |
56
57
  | `q` | quit (during a scan: stop and show partial results; press again to force quit) |
57
58
 
@@ -79,7 +80,7 @@ windscout -t 40 -a # longer connect timeout, also list
79
80
  | `-a, --all` | include failed attempts in the final table | off |
80
81
  | `-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
82
  | `-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 | |
83
+ | `-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
84
 
84
85
  ## Connect-only mode
85
86
 
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
 
@@ -61,6 +61,17 @@ async function offerConnect(choices, timeout) {
61
61
  return;
62
62
  }
63
63
  }
64
+ /** u in the picker: update, then run the new version in this one's place, with the same arguments */
65
+ function updateAndRestart() {
66
+ const status = runUpdate();
67
+ if (status) {
68
+ console.error(yellow(`update failed (exit ${status})`));
69
+ process.exitCode = status;
70
+ return;
71
+ }
72
+ console.log(green("updated · restarting windscout"));
73
+ process.exitCode = spawnSync(process.execPath, [...process.execArgv, ...process.argv.slice(1)], { stdio: "inherit" }).status ?? 1;
74
+ }
64
75
  async function main() {
65
76
  const { values: a } = parseArgs({
66
77
  options: {
@@ -78,19 +89,10 @@ async function main() {
78
89
  return console.log(HELP);
79
90
  checkUpdate(); // runs while the app starts and the user picks, before the VPN starts switching
80
91
  if (a.version) {
81
- console.log(`windscout ${VERSION}`);
82
92
  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
- }
93
+ console.log(versionLines().join("\n"));
94
+ if (canUpdate() && process.stdin.isTTY && out.isTTY && /^y(es)?$/i.test(await ask("update now? (y/n) ")))
95
+ process.exitCode = runUpdate();
94
96
  return;
95
97
  }
96
98
  let timeout = a.timeout ? Number(a.timeout) : undefined;
@@ -116,6 +118,8 @@ async function main() {
116
118
  });
117
119
  if (!choice)
118
120
  return;
121
+ if (choice === "update")
122
+ return updateAndRestart();
119
123
  countries = choice.countries;
120
124
  protocols ??= choice.protocols;
121
125
  speedTest = choice.speed;
@@ -134,9 +138,8 @@ async function main() {
134
138
  console.log(yellow("interrupted, showing partial results"));
135
139
  const choices = report(scan.results, protocols, { all: a.all, speedTest });
136
140
  await checkUpdate();
137
- const notice = updateNotice();
138
- if (notice)
139
- console.error("\n" + yellow(notice));
141
+ if (updateNotice())
142
+ console.error("\n" + versionLines().join("\n"));
140
143
  if (tty && choices.length)
141
144
  await offerConnect(choices, timeout);
142
145
  }
@@ -1,14 +1,14 @@
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 */
11
+ /** Resolves to the choice (also saved for next time), null when the user quits, or "update" for u */
12
12
  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);
@@ -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 - 21 - version.length);
29
29
  cursor = Math.max(0, Math.min(cursor, matches.length - 1));
30
30
  if (cursor < top)
31
31
  top = cursor;
@@ -89,9 +89,7 @@ export function pick(saved, { timeout, speedTest, firewall }) {
89
89
  ];
90
90
  if (message)
91
91
  lines.push(centerIn(yellow(message), width));
92
- lines.push("", centerIn(dim(repoLink), width));
93
- if (notice)
94
- lines.push(centerIn(yellow(notice), width));
92
+ lines.push("", ...version.map((l) => centerIn(l, width)), centerIn(dim(repoLink), width));
95
93
  const at = draw(lines);
96
94
  // click map, in screen coordinates
97
95
  let px = at.left + 2; // "│ " before the protocol line
@@ -116,13 +114,13 @@ export function pick(saved, { timeout, speedTest, firewall }) {
116
114
  };
117
115
  };
118
116
  let closed = false;
119
- checkUpdate().then(() => updateNotice() && !closed && render()); // the check usually lands after the first draw
117
+ checkUpdate().then(() => !closed && render()); // the check usually lands after the first draw
120
118
  const finish = (value) => {
121
119
  closed = true;
122
120
  out.off("resize", render);
123
121
  input.removeAllListeners("keypress");
124
122
  input.removeAllListeners("mouse");
125
- if (!value)
123
+ if (!value || value === "update")
126
124
  leaveScreen();
127
125
  resolve(value);
128
126
  };
@@ -257,6 +255,8 @@ export function pick(saved, { timeout, speedTest, firewall }) {
257
255
  firewall = !firewall;
258
256
  else if (k === "t")
259
257
  editing = true, seconds = "";
258
+ else if (k === "u" && canUpdate())
259
+ return finish("update");
260
260
  else if (/^[1-9]$/.test(str ?? "") && PROTOCOLS[Number(str) - 1])
261
261
  flip(protos, PROTOCOLS[Number(str) - 1]);
262
262
  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.7",
4
4
  "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {