spinuptui 1.2.0-alpha.1 → 1.2.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -10
  2. package/README.md +5 -10
  3. package/package.json +1 -1
  4. package/src/api/client.ts +15 -0
  5. package/src/api/types.ts +3 -0
  6. package/src/config.ts +0 -23
  7. package/src/dev/mockClient.ts +30 -7
  8. package/src/index.tsx +17 -16
  9. package/src/lib/appUpdate.ts +7 -33
  10. package/src/lib/cliOffline.ts +418 -0
  11. package/src/lib/offline.test.ts +181 -0
  12. package/src/lib/offline.ts +422 -0
  13. package/src/lib/spinupweb.test.ts +22 -0
  14. package/src/lib/spinupweb.ts +11 -0
  15. package/src/lib/stackCache.test.ts +43 -0
  16. package/src/lib/stackCache.ts +35 -1
  17. package/src/ui/App.tsx +12 -10
  18. package/src/ui/Details.tsx +8 -2
  19. package/src/ui/Explain.tsx +2 -15
  20. package/src/ui/Header.tsx +0 -2
  21. package/src/ui/Help.tsx +3 -2
  22. package/src/ui/Onboarding.tsx +71 -3
  23. package/src/ui/components.tsx +2 -1
  24. package/src/ui/offlineRun.tsx +82 -0
  25. package/src/ui/store.tsx +76 -10
  26. package/src/ui/views/AccountSlugPrompt.tsx +102 -0
  27. package/src/ui/views/Browser.tsx +13 -5
  28. package/src/ui/views/Dashboard.tsx +3 -1
  29. package/src/ui/views/Search.tsx +8 -6
  30. package/src/ui/views/ServerActions.tsx +108 -11
  31. package/src/ui/views/SiteOffline.tsx +171 -0
  32. package/src/ui/views/Stacks.tsx +26 -9
  33. package/src/lib/appUpdate.test.ts +0 -26
  34. package/src/lib/beacon.test.ts +0 -140
  35. package/src/lib/beacon.ts +0 -349
  36. package/src/lib/beaconSetup.test.ts +0 -65
  37. package/src/lib/beaconSetup.ts +0 -143
  38. package/src/lib/cliBeacon.ts +0 -290
  39. package/src/lib/prompt.ts +0 -47
  40. package/src/modules/beacon.ts +0 -51
  41. package/src/modules/contract.ts +0 -61
  42. package/src/modules/index.ts +0 -34
  43. package/src/ui/views/BeaconSetup.tsx +0 -268
  44. package/src/ui/views/Modules.tsx +0 -196
package/CHANGELOG.md CHANGED
@@ -11,17 +11,16 @@ version; such changes are called out here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
- ## [1.2.0-alpha.1] - 2026-10-17
14
+ ## [1.2.1] - 2026-10-27
15
15
 
16
- A pre-release, published on npm's `next` tag (`bun add -g spinuptui@next --no-cache`). `@latest` and Homebrew stay on 1.1.2. The stable 1.2.0 will roll these notes up with the next modules.
16
+ ### Fixed
17
+ - **WordPress versions no longer go stale.** The version shown for a site was read once, when the site was first identified, and never again, so sites updated outside SpinupTUI (auto-updates, wp-admin, deploys) kept showing the old version, and `r` didn't help. Versions older than a day are now re-checked in the background when the site list loads, one site at a time per server, stopping at a server's first failure so unattended SSH can't trip fail2ban. A reading that couldn't be refreshed shows its age in the Stacks list, and the Details pane always says when it was taken.
18
+ - **`w` opens the site or server you picked, not the SpinupWP dashboard.** Deep links need the account slug, which SpinupWP's API doesn't provide and first-run setup never asked for, so unless you had added `accountSlug` to `config.json` by hand, `w` quietly fell back to the dashboard. Now `w` asks for the slug the first time it needs it (pasting any dashboard URL works), saves it to the current account and opens the right page. First-run setup also asks for it, as an optional step after the token.
17
19
 
18
- ### Added
19
- - **Beacon: `spinuptui beacon send` pages you on Telegram.** One command any script, cron job or agent can use instead of its own `curl` to the Telegram API. Messages go out as plain text with link previews off; anything over Telegram's 4096-character limit is split into numbered parts instead of being rejected; sends are retried (honoring Telegram's rate-limit hints) and logged. Exit codes tell a caller what happened: `0` delivered, `1` not configured (nothing attempted), `2` delivery failed. No Telegram bot yet? `spinuptui beacon setup` walks you through making one with @BotFather, checks the token, and finds your chat by itself once you message the bot. No digging for a chat ID. It saves nothing until a test message has arrived. Scripts can use `spinuptui beacon add telegram <name> --env-file <path>` instead. `beacon test`, `beacon status` and `beacon remove` round it out. See [docs/beacon.md](docs/beacon.md).
20
- - **A Modules tab (`6`).** Everything SpinupTUI can keep doing after you close it, with its status on this machine and set up from the same place: select Beacon and press `⏎` for the guided Telegram setup, or `t` for a test page. Modules still on the way (Scheduler, Sentinel, Samurai) are listed as coming soon.
21
- - **`spinuptui modules`** lists SpinupTUI's modules (the features that keep working after the TUI is closed) and their status. Beacon is the first; the module contract is in [docs/2026-10-16_beacon-and-module-contract-spec.md](docs/2026-10-16_beacon-and-module-contract-spec.md).
20
+ ## [1.2.0] - 2026-10-21
22
21
 
23
- ### Fixed
24
- - **The update hint understands pre-releases.** Someone running a pre-release (say `1.2.0-alpha.1`) is now told when the matching stable release ships; before, the two counted as the same version.
22
+ ### Added
23
+ - **Take a server's sites offline, and bring them back.** For a vulnerability you can't patch yet: `spinuptui offline <server>… --yes` (or **Take all sites offline** in the `a` menu) disables every site on the server through SpinupWP, so nginx stops answering for them and no request reaches PHP. Files, databases and certificates are untouched. `spinuptui online <server>… --yes` re-enables exactly the sites it took down, never one disabled for another reason, and a re-run finishes anything that failed. Without `--yes` both print the plan. One site at a time works too: `O` on a site, or `--site <domain>` on either command; the confirmation says how many other sites on its server stay up, since PHP is shared. Uptime Kuma monitors for those sites sit in a maintenance window until they're back, and offline servers are flagged `⏻ off` in the Servers list and on the Overview. See [docs/taking-sites-offline.md](docs/taking-sites-offline.md).
25
24
 
26
25
  ## [1.1.2] - 2026-10-15
27
26
 
@@ -1497,8 +1496,9 @@ Initial tagged release.
1497
1496
  ### Notes
1498
1497
  - Read-only release: works with a SpinupWP **Read Only** API token.
1499
1498
 
1500
- [Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v1.2.0-alpha.1...HEAD
1501
- [1.2.0-alpha.1]: https://github.com/mwender/spinupwp-tui/compare/v1.1.2...v1.2.0-alpha.1
1499
+ [Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v1.2.1...HEAD
1500
+ [1.2.1]: https://github.com/mwender/spinupwp-tui/compare/v1.2.0...v1.2.1
1501
+ [1.2.0]: https://github.com/mwender/spinupwp-tui/compare/v1.1.2...v1.2.0
1502
1502
  [1.1.2]: https://github.com/mwender/spinupwp-tui/compare/v1.1.1...v1.1.2
1503
1503
  [1.1.1]: https://github.com/mwender/spinupwp-tui/compare/v1.1.0...v1.1.1
1504
1504
  [1.1.0]: https://github.com/mwender/spinupwp-tui/compare/v1.0.0...v1.1.0
package/README.md CHANGED
@@ -60,6 +60,7 @@
60
60
  - **Purge cache** (`P`) — clear page + object cache together.
61
61
  - **Server actions** (`a`) — reboot or restart a service, with the real reason a
62
62
  reboot is pending. → [docs/server-actions.md](docs/server-actions.md)
63
+ - **Take sites offline** (`a` for a whole server, `O` for one site, or `spinuptui offline`) — for a vulnerability you can't patch yet: nginx stops answering for those sites, so no request reaches PHP through them. Bringing them back re-enables exactly those sites, and Uptime Kuma stays quiet meanwhile. → [docs/taking-sites-offline.md](docs/taking-sites-offline.md)
63
64
  - **Create & connect a server** (`c` / `V`) — provision a server, then wire it up
64
65
  end-to-end (DNS, a vanity site, HTTPS, SSH-key handoff).
65
66
  → [docs/creating-connecting-servers.md](docs/creating-connecting-servers.md)
@@ -76,11 +77,6 @@
76
77
  register/recalibrate/remove it — plus a cache/outage doctor and alert
77
78
  wiring. → [docs/site-monitoring.md](docs/site-monitoring.md) ·
78
79
  [docs/uptime-kuma.md](docs/uptime-kuma.md)
79
- - **Modules** (`6`, pre-release: `bun add -g spinuptui@next --no-cache`) — the parts of SpinupTUI that keep working after you close it,
80
- each set up from the same screen. First up, **Beacon**: `spinuptui beacon send`
81
- pages you on Telegram from any cron job, watcher or agent, with retries, long
82
- messages split instead of dropped, and a send log. A guided setup creates the
83
- bot connection for you. → [docs/beacon.md](docs/beacon.md)
84
80
  - **Non-interactive CLI subcommands for external tooling** — `spinuptui ssh <domain>`
85
81
  resolves a domain to a live SSH target with a live connectivity probe;
86
82
  `spinuptui ssh-exec <domain> -- <command>` runs a command over SSH, but only
@@ -309,8 +305,9 @@ These can be set in `config.json` or via an environment variable:
309
305
  - **`accountSlug`** / `SPINUPWP_ACCOUNT_SLUG` — your SpinupWP account/team slug
310
306
  (the first path segment in a SpinupWP URL, e.g. `acme-agency` in
311
307
  `https://spinupwp.app/acme-agency/servers/12345`). The API
312
- doesn't expose it, so set it to enable the `w` deep links into the web app.
313
- Without it, `w` opens the SpinupWP dashboard root.
308
+ doesn't expose it, so it's needed for the `w` deep links into the web app.
309
+ You don't have to set it by hand: first-run setup asks for it (optional), and
310
+ `w` asks the first time it's needed, then saves it to the current account.
314
311
  - **`sshUser`** / `SPINUPWP_SSH_USER` — override the SSH user for the health view
315
312
  and stack probes (see "Server health" below).
316
313
  - **`localSync`** / `SPINUPWP_LOCAL_SYNC` — opt-in for the **Pull production →
@@ -338,7 +335,7 @@ These can be set in `config.json` or via an environment variable:
338
335
 
339
336
  | Key | Action |
340
337
  | --- | --- |
341
- | `1`…`6` | Switch tabs: Dashboard · Servers · Stacks · Search · Events · Modules |
338
+ | `1`…`5` | Switch tabs: Dashboard · Servers · Stacks · Search · Events |
342
339
  | `↑`/`↓` or `j`/`k` | Move selection |
343
340
  | `Enter` / `→` | Drill in (server → its sites) |
344
341
  | `←` / `Esc` | Go back / collapse |
@@ -374,8 +371,6 @@ These can be set in `config.json` or via an environment variable:
374
371
  | `w` | Open the selected server/site in the SpinupWP web app |
375
372
  | `/` | Jump to global search |
376
373
  | `A` | Accounts — switch, add, rename, or remove SpinupWP accounts (from anywhere) |
377
- | `⏎` / `s` | Set up the selected module — Beacon opens its guided Telegram setup (Modules tab) |
378
- | `t` | Send a test page through Beacon (Modules tab, once Beacon is set up) |
379
374
  | `r` | Refresh data from the API |
380
375
  | `i` | Explain the current screen (what each pane and key does) |
381
376
  | `?` | Toggle the help overlay |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spinuptui",
3
- "version": "1.2.0-alpha.1",
3
+ "version": "1.2.1",
4
4
  "description": "SpinupTUI — a terminal dashboard for browsing, monitoring, and managing your SpinupWP servers and sites.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api/client.ts CHANGED
@@ -109,6 +109,8 @@ export interface SpinupWPClientLike {
109
109
  upgradeSitePhp(siteId: number, phpVersion: string): Promise<{ event_id: number }>
110
110
  rebootServer(serverId: number): Promise<{ event_id: number }>
111
111
  restartService(serverId: number, service: ServerService): Promise<{ event_id: number }>
112
+ disableSite(siteId: number): Promise<{ event_id: number }>
113
+ enableSite(siteId: number): Promise<{ event_id: number }>
112
114
  }
113
115
 
114
116
  const MAX_PAGES = 100 // hard safety cap when auto-paginating
@@ -482,4 +484,17 @@ export class SpinupWPClient implements SpinupWPClientLike {
482
484
  restartService(serverId: number, service: ServerService): Promise<{ event_id: number }> {
483
485
  return this.mutate<{ event_id: number }>(`/servers/${serverId}/services/${service}/restart`, "POST")
484
486
  }
487
+
488
+ // Take a site offline without touching its files, database or certificate.
489
+ // Verified on a test box: nginx drops the site's server block, so port 80
490
+ // returns an empty reply and 443 fails the TLS handshake — no request reaches
491
+ // PHP. The site's PHP-FPM pool and cron stay in place. Async → event_id.
492
+ disableSite(siteId: number): Promise<{ event_id: number }> {
493
+ return this.mutate<{ event_id: number }>(`/sites/${siteId}/disable`, "POST")
494
+ }
495
+
496
+ // Undo disableSite: the site serves traffic again exactly as before.
497
+ enableSite(siteId: number): Promise<{ event_id: number }> {
498
+ return this.mutate<{ event_id: number }>(`/sites/${siteId}/enable`, "POST")
499
+ }
485
500
  }
package/src/api/types.ts CHANGED
@@ -120,6 +120,9 @@ export interface Site {
120
120
  } | null
121
121
  basic_auth?: { enabled: boolean; username?: string | null } | null
122
122
  subdomain?: { enabled: boolean; url: string | null } | null
123
+ // Set while the site is disabled (POST /sites/{id}/disable): nginx stops
124
+ // serving every domain on it, so no request reaches PHP. Null when live.
125
+ disabled_at?: string | null
123
126
  created_at: string
124
127
  status: string
125
128
  }
package/src/config.ts CHANGED
@@ -103,27 +103,6 @@ export interface AppConfig {
103
103
  // running version with no notes shown — nothing meaningful to announce). A
104
104
  // mismatch against the running version is what triggers the overlay.
105
105
  lastSeenVersion: string | null
106
- // Beacon's notification channels (see lib/beacon.ts and
107
- // docs/2026-10-16_beacon-and-module-contract-spec.md). Machine-level like the
108
- // Kuma login: one operator's pager serves every account. Holds bot tokens, so
109
- // it relies on the file's 600 mode. The env channel (SPINUPTUI_BEACON_*) is
110
- // resolved in lib/beacon.ts, not here, so this is only what's stored.
111
- beacon: BeaconStored | null
112
- }
113
-
114
- export interface BeaconTelegramChannel {
115
- type: "telegram"
116
- botToken: string
117
- chatId: string
118
- }
119
-
120
- // A union so later transports (Slack, ntfy, a client-facing Basecamp channel)
121
- // are additive.
122
- export type BeaconChannel = BeaconTelegramChannel
123
-
124
- export interface BeaconStored {
125
- defaultChannel?: string
126
- channels?: Record<string, BeaconChannel>
127
106
  }
128
107
 
129
108
  export interface ServerProviderRef {
@@ -223,7 +202,6 @@ export interface StoredConfig {
223
202
  uptimeKuma?: UptimeKumaConn
224
203
  kumaMonitors?: Record<string, KumaMonitorRef>
225
204
  lastSeenVersion?: string
226
- beacon?: BeaconStored
227
205
  }
228
206
 
229
207
  // ---- Accounts (profiles) -------------------------------------------------------
@@ -509,7 +487,6 @@ export function loadConfig(): AppConfig {
509
487
  })(),
510
488
  kumaMonitors: stored.kumaMonitors ?? {},
511
489
  lastSeenVersion: stored.lastSeenVersion?.trim() || null,
512
- beacon: stored.beacon ?? null,
513
490
  }
514
491
  }
515
492
 
@@ -45,6 +45,16 @@ export function createMockClient(): SpinupWPClientLike {
45
45
  let nextEventId = nextIdAfter(events, 9100)
46
46
  const eventPolls = new Map<number, number>()
47
47
 
48
+ function advance(e: Event): void {
49
+ if (e.status !== "running") return
50
+ const polls = (eventPolls.get(e.id) ?? 0) + 1
51
+ eventPolls.set(e.id, polls)
52
+ if (polls >= EVENT_POLLS_BEFORE_DONE) {
53
+ e.status = "deployed"
54
+ e.finished_at = new Date().toISOString()
55
+ }
56
+ }
57
+
48
58
  function pushEvent(name: string, serverId: number | null, output: string): number {
49
59
  const id = nextEventId++
50
60
  const now = new Date().toISOString()
@@ -230,20 +240,19 @@ export function createMockClient(): SpinupWPClientLike {
230
240
  return { event_id }
231
241
  },
232
242
 
243
+ // Reading an event (singly or in the list) counts as a poll, so callers
244
+ // that batch-check via listEvents see events finish too.
233
245
  async listEvents(maxPages) {
234
246
  const limit = (maxPages ?? 3) * 100
235
- return events.slice(0, limit).map((e) => ({ ...e }))
247
+ const page = events.slice(0, limit)
248
+ for (const e of page) advance(e)
249
+ return page.map((e) => ({ ...e }))
236
250
  },
237
251
 
238
252
  async getEvent(id) {
239
253
  const found = events.find((e) => e.id === id)
240
254
  if (!found) throw new Error(`dev mode: no such event ${id}`)
241
- const polls = (eventPolls.get(id) ?? 0) + 1
242
- eventPolls.set(id, polls)
243
- if (polls >= EVENT_POLLS_BEFORE_DONE && found.status === "running") {
244
- found.status = "deployed"
245
- found.finished_at = new Date().toISOString()
246
- }
255
+ advance(found)
247
256
  return { ...found }
248
257
  },
249
258
 
@@ -266,5 +275,19 @@ export function createMockClient(): SpinupWPClientLike {
266
275
  const event_id = pushEvent(`server.service.${service}.restarted`, serverId, `${service} restarted on ${server?.name ?? serverId}`)
267
276
  return { event_id }
268
277
  },
278
+
279
+ async disableSite(siteId) {
280
+ const site = sites.find((s) => s.id === siteId)
281
+ if (site) site.disabled_at = new Date().toISOString()
282
+ const event_id = pushEvent("site.disabled", site?.server_id ?? null, `Disabling ${site?.domain ?? siteId}`)
283
+ return { event_id }
284
+ },
285
+
286
+ async enableSite(siteId) {
287
+ const site = sites.find((s) => s.id === siteId)
288
+ if (site) site.disabled_at = null
289
+ const event_id = pushEvent("site.enabled", site?.server_id ?? null, `Enabling ${site?.domain ?? siteId}`)
290
+ return { event_id }
291
+ },
269
292
  }
270
293
  }
package/src/index.tsx CHANGED
@@ -23,7 +23,7 @@ import { resolveIncidents } from "./lib/cliIncidents.ts"
23
23
  import { resolveSiblings } from "./lib/cliSiblings.ts"
24
24
  import { execSshCommand } from "./lib/sshExec.ts"
25
25
  import { runPullFiles, runPullDb } from "./lib/cliPull.ts"
26
- import { runBeacon, runModules } from "./lib/cliBeacon.ts"
26
+ import { runOffline, runOnline } from "./lib/cliOffline.ts"
27
27
 
28
28
  const args = process.argv.slice(2)
29
29
  const positionals = args.filter((a) => !a.startsWith("-"))
@@ -90,17 +90,18 @@ Usage:
90
90
  Import production's database into the linked local copy,
91
91
  rewriting URLs. OVERWRITES your local database: needs
92
92
  both --yes and "localSync": true in the config.
93
- spinuptui beacon setup Guided Telegram setup: make a bot, find your chat,
94
- send a test (interactive; also in the app's Modules tab, 6)
95
- spinuptui beacon send [--to <channel>] [--silent] [<message> | -]
96
- Page yourself through Beacon (Telegram). Reads the
97
- message from stdin when it's \`-\` or absent. Exit 0 sent,
98
- 1 not configured / usage, 2 delivery failed.
99
- spinuptui beacon add telegram <name> --env-file <path> [--default]
100
- Add a channel from a file holding TELEGRAM_BOT_TOKEN and
101
- TELEGRAM_CHAT_ID; sends a test message before saving.
102
- spinuptui beacon test | status | remove <name>
103
- spinuptui modules List SpinupTUI's modules and their status (--json)
93
+ spinuptui offline List servers with sites offline (--json)
94
+ spinuptui offline <server>… | --all --yes
95
+ Take every site on those servers offline: nginx stops
96
+ answering, so no request reaches PHP. Files, databases
97
+ and certificates are untouched. Without --yes, prints
98
+ the plan and changes nothing.
99
+ spinuptui offline --site <domain> [--server <name>] --yes
100
+ Just that site. Its server's other sites (and PHP) stay
101
+ up, so this protects the site, not the server.
102
+ spinuptui online <server>… | --all | --site <domain> --yes
103
+ Bring back exactly the sites \`offline\` took down.
104
+ Servers are matched by exact name, IP or id.
104
105
  spinuptui --version Print the version
105
106
  spinuptui --help Show this help
106
107
 
@@ -284,12 +285,12 @@ if (command === "pull") {
284
285
  process.exit(result.ok ? 0 : 1)
285
286
  }
286
287
 
287
- if (command === "beacon") {
288
- process.exit(await runBeacon(args.slice(args.indexOf("beacon") + 1)))
288
+ if (command === "offline") {
289
+ process.exit(await runOffline(args.slice(args.indexOf("offline") + 1)))
289
290
  }
290
291
 
291
- if (command === "modules") {
292
- process.exit(await runModules(args.slice(1)))
292
+ if (command === "online") {
293
+ process.exit(await runOnline(args.slice(args.indexOf("online") + 1)))
293
294
  }
294
295
 
295
296
  // `spinuptui login` forces the onboarding wizard even when a token already exists,
@@ -35,47 +35,21 @@ function cachePath(): string {
35
35
  return join(configDir(), "update-check.json")
36
36
  }
37
37
 
38
- // Parse "v1.2.3" / "1.2.3" / "1.2.3-alpha.1" → numbers + pre-release tag; null
39
- // when it doesn't look like a version.
40
- function parseVersion(s: string): { nums: [number, number, number]; pre: string | null } | null {
41
- const m = s.trim().replace(/^v/i, "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/)
42
- return m ? { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null } : null
43
- }
44
-
45
- // SemVer pre-release precedence: dot-separated identifiers compared left to
46
- // right, numeric ones numerically and below alphanumeric ones; a shorter set of
47
- // otherwise-equal identifiers sorts first.
48
- function comparePre(a: string, b: string): number {
49
- const x = a.split(".")
50
- const y = b.split(".")
51
- for (let i = 0; i < Math.max(x.length, y.length); i++) {
52
- if (x[i] === undefined) return -1
53
- if (y[i] === undefined) return 1
54
- const nx = /^\d+$/.test(x[i]!)
55
- const ny = /^\d+$/.test(y[i]!)
56
- if (nx && ny) {
57
- const d = Number(x[i]) - Number(y[i])
58
- if (d) return d
59
- } else if (nx !== ny) return nx ? -1 : 1
60
- else if (x[i] !== y[i]) return x[i]! < y[i]! ? -1 : 1
61
- }
62
- return 0
38
+ // Parse "v1.2.3" / "1.2.3" → [1,2,3]; null when it doesn't look like a version.
39
+ function parseVersion(s: string): [number, number, number] | null {
40
+ const m = s.trim().replace(/^v/i, "").match(/^(\d+)\.(\d+)\.(\d+)/)
41
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
63
42
  }
64
43
 
65
44
  // Strictly-newer comparison (latest > current). Unparseable → false (never nag).
66
- // A release outranks its own pre-releases, so someone on 1.2.0-alpha.1 is told
67
- // when 1.2.0 ships.
68
- export function isNewer(latest: string, current: string): boolean {
45
+ function isNewer(latest: string, current: string): boolean {
69
46
  const a = parseVersion(latest)
70
47
  const b = parseVersion(current)
71
48
  if (!a || !b) return false
72
49
  for (let i = 0; i < 3; i++) {
73
- if (a.nums[i] !== b.nums[i]) return a.nums[i]! > b.nums[i]!
50
+ if (a[i] !== b[i]) return a[i] > b[i]
74
51
  }
75
- if (a.pre === b.pre) return false
76
- if (a.pre === null) return true
77
- if (b.pre === null) return false
78
- return comparePre(a.pre, b.pre) > 0
52
+ return false
79
53
  }
80
54
 
81
55
  function readCache(): CacheFile | null {