spinuptui 0.19.0
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/CHANGELOG.md +943 -0
- package/LICENSE +21 -0
- package/README.md +856 -0
- package/bin/spinuptui.js +13 -0
- package/package.json +55 -0
- package/src/api/client.ts +354 -0
- package/src/api/types.ts +206 -0
- package/src/config.ts +309 -0
- package/src/dev/devMode.ts +7 -0
- package/src/dev/fixtures.ts +238 -0
- package/src/dev/mockClient.ts +214 -0
- package/src/index.tsx +68 -0
- package/src/lib/appUpdate.ts +206 -0
- package/src/lib/awsSigV4.ts +133 -0
- package/src/lib/cloneDomains.ts +112 -0
- package/src/lib/cloneLog.ts +85 -0
- package/src/lib/dbBackup.ts +198 -0
- package/src/lib/dbSync.ts +253 -0
- package/src/lib/discover.ts +165 -0
- package/src/lib/dns.ts +144 -0
- package/src/lib/dnsCache.ts +94 -0
- package/src/lib/dnsQuery.ts +180 -0
- package/src/lib/dnsRecords.ts +411 -0
- package/src/lib/format.ts +106 -0
- package/src/lib/gitDeployKey.ts +0 -0
- package/src/lib/gitStatus.ts +37 -0
- package/src/lib/jobs.ts +32 -0
- package/src/lib/keychain.ts +49 -0
- package/src/lib/local.ts +116 -0
- package/src/lib/mediaFallback.ts +288 -0
- package/src/lib/open.ts +103 -0
- package/src/lib/phpEol.ts +151 -0
- package/src/lib/probe.ts +183 -0
- package/src/lib/providers.ts +284 -0
- package/src/lib/providersCache.ts +105 -0
- package/src/lib/releaseNotes.ts +67 -0
- package/src/lib/serverClone.ts +782 -0
- package/src/lib/serverCreate.ts +161 -0
- package/src/lib/siteDoctor.ts +260 -0
- package/src/lib/siteFingerprint.ts +108 -0
- package/src/lib/spinupweb.ts +33 -0
- package/src/lib/ssh.ts +669 -0
- package/src/lib/stack.ts +78 -0
- package/src/lib/stackCache.ts +113 -0
- package/src/lib/theme.ts +54 -0
- package/src/lib/ubuntuEol.ts +124 -0
- package/src/lib/uptimeKuma.ts +551 -0
- package/src/lib/vanitySite.ts +297 -0
- package/src/lib/wpInventory.ts +153 -0
- package/src/ui/App.tsx +265 -0
- package/src/ui/Details.tsx +403 -0
- package/src/ui/Explain.tsx +158 -0
- package/src/ui/Header.tsx +117 -0
- package/src/ui/Help.tsx +296 -0
- package/src/ui/List.tsx +95 -0
- package/src/ui/Onboarding.tsx +93 -0
- package/src/ui/Splash.tsx +51 -0
- package/src/ui/StatusBar.tsx +60 -0
- package/src/ui/components.tsx +392 -0
- package/src/ui/store.tsx +4253 -0
- package/src/ui/views/Browser.tsx +365 -0
- package/src/ui/views/CloneWizard.tsx +932 -0
- package/src/ui/views/Dashboard.tsx +149 -0
- package/src/ui/views/DbBackup.tsx +228 -0
- package/src/ui/views/DbSync.tsx +307 -0
- package/src/ui/views/Discover.tsx +271 -0
- package/src/ui/views/DnsInventory.tsx +580 -0
- package/src/ui/views/DnsRecords.tsx +562 -0
- package/src/ui/views/Events.tsx +98 -0
- package/src/ui/views/Forgotten.tsx +165 -0
- package/src/ui/views/GrantKey.tsx +504 -0
- package/src/ui/views/Health.tsx +322 -0
- package/src/ui/views/HttpsToggle.tsx +195 -0
- package/src/ui/views/KumaSite.tsx +571 -0
- package/src/ui/views/LocalLink.tsx +220 -0
- package/src/ui/views/MediaFallback.tsx +180 -0
- package/src/ui/views/NewServer.tsx +750 -0
- package/src/ui/views/PhpUpgrade.tsx +301 -0
- package/src/ui/views/ProviderConnect.tsx +527 -0
- package/src/ui/views/PurgeCache.tsx +186 -0
- package/src/ui/views/ReleaseNotes.tsx +84 -0
- package/src/ui/views/Search.tsx +448 -0
- package/src/ui/views/ServerActions.tsx +328 -0
- package/src/ui/views/Stacks.tsx +371 -0
- package/src/ui/views/SudoConnect.tsx +358 -0
- package/src/ui/views/VanityNewSite.tsx +407 -0
- package/src/ui/views/WpInventory.tsx +218 -0
- package/src/version.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/assets/banner.png" alt="SpinupWP TUI" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">SpinupTUI</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
A fast, keyboard-driven terminal control center for your
|
|
9
|
+
<a href="https://spinupwp.com">SpinupWP</a> account — browse and monitor your
|
|
10
|
+
servers and sites, and run local-dev workflows against them.<br>
|
|
11
|
+
Built with <a href="https://opentui.com">OpenTUI</a> and <a href="https://bun.sh">Bun</a>.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
Once you're in, the dashboard looks like this:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
◆ SpinupTUI v0.9.0 1 Dashboard 2 Servers 3 Stacks 4 Search 5 Events 20 servers · 171 sites
|
|
18
|
+
|
|
19
|
+
┌──────────────┐ ┌───────────────┐ ┌───────────────────┐ ┌──────────────────────┐
|
|
20
|
+
│ Servers │ │ Sites │ │ Fleet Disk │ │ WP Updates │
|
|
21
|
+
│ 20 │ │ 171 │ │ 22% │ │ 359 │
|
|
22
|
+
│ 20 connected │ │ 139 WordPress │ │ 616.3 GB / 2.8 TB │ │ 217 plugin · 67 core │
|
|
23
|
+
└──────────────┘ └───────────────┘ └───────────────────┘ └──────────────────────┘
|
|
24
|
+
┌─ Disk usage by server ───────────────┐ ┌─ Needs attention (27) ──────────────┐
|
|
25
|
+
│ web1.example.com ██████░░░ 60% │ │ • db1.example.com — … │
|
|
26
|
+
│ web2.example.com ██████░░░ 57% │ │ • web3.example.org — OS … │
|
|
27
|
+
└──────────────────────────────────────┘ └─────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Fleet dashboard** — at-a-glance health of every server: connection status,
|
|
33
|
+
disk usage bars, pending reboots/SpinupWP platform upgrades, WordPress update
|
|
34
|
+
counts, and a recent activity feed. Servers running an **end-of-life Ubuntu
|
|
35
|
+
release** are flagged too — a `⚠ os` badge in the Servers list, a red "Ubuntu …
|
|
36
|
+
EOL" line in a server's detail panel, and an entry in the dashboard's "Needs
|
|
37
|
+
attention" list pointing at the clone wizard (`C`).
|
|
38
|
+
- **Server & site browser** — a three-pane navigator. Pick a server, see its
|
|
39
|
+
sites, drill into full details (PHP version, HTTPS, page cache, backups, Git
|
|
40
|
+
deployment, WP updates, and more). Each site row carries an at-a-glance **status
|
|
41
|
+
column** — `👤🔑` granted SSH keys, `H` HTTPS, `C` page cache, `B` backups (lit
|
|
42
|
+
when on, faint `·` when off), plus `◆` linked and `↑N` pending updates — aligned
|
|
43
|
+
into columns across every row, with a compact key in the Sites panel title.
|
|
44
|
+
- **Stack detection & fleet composition** — the Stacks tab classifies every site
|
|
45
|
+
as Standard WP, Bedrock, or Non-WP, with a fleet-wide PHP version breakdown
|
|
46
|
+
(EOL versions flagged). Press `d` to SSH-probe a site's actual stack — naming
|
|
47
|
+
WHMCS, Laravel, Static HTML, and WordPress versions the API can't tell you —
|
|
48
|
+
or `D` to probe a whole stack at once. (See "Stack detection" below.)
|
|
49
|
+
- **Global search** — fuzzy search across every server and site at once by name,
|
|
50
|
+
domain, or IP. Tab onto a result to act on it (open, SpinupWP, PHP upgrade,
|
|
51
|
+
health) right from the results, without leaving the search.
|
|
52
|
+
- **Events feed** — recent provisioning and operation activity, with per-event
|
|
53
|
+
detail and output.
|
|
54
|
+
- **Live server health** — press `h` on any server for a real-time view over
|
|
55
|
+
SSH: CPU (aggregate + per-core + sparkline), load, memory/swap, disk mounts,
|
|
56
|
+
and top processes. Polls every few seconds. (See "Server health" below.)
|
|
57
|
+
- **Installed plugins & themes** — press `p` on a site for its real `wp plugin
|
|
58
|
+
list` / `wp theme list` over SSH: every plugin and theme with its status,
|
|
59
|
+
current version, and available update — the detail the API only exposes as bare
|
|
60
|
+
counts. Read-only, combined scrollable list with per-section update badges.
|
|
61
|
+
Detects the WordPress directory itself, so it works on `/public/` and Bedrock
|
|
62
|
+
installs alike (even sites SpinupWP misclassifies as non-WordPress). (See
|
|
63
|
+
"Installed plugins & themes" below.)
|
|
64
|
+
- **Open in browser** — press `o` on any site to open it in your default browser.
|
|
65
|
+
- **Link local working copies** — press `L` on a site to link its local checkout
|
|
66
|
+
(a path plus the local URL where you serve it), then open it with `t` (a
|
|
67
|
+
terminal there) or `v` (its local URL). The Stacks tab can **auto-discover**
|
|
68
|
+
copies (`S`) by git remote / Bedrock `WP_HOME` / folder name, and **report**
|
|
69
|
+
managed sites that still need one (`f`). Linked sites show a `◆` marker, and a
|
|
70
|
+
linked checkout shows its local git drift (`⇡N unpushed` / `● uncommitted`).
|
|
71
|
+
(See "Local working copies" below.)
|
|
72
|
+
- **SSH into a site** — press `s` on a site to open a new terminal already running
|
|
73
|
+
`ssh` into it (`{site_user}@{server_ip}`).
|
|
74
|
+
- **DNS migration lens** — press `n` on a site (or `N` on a server) to see the DNS
|
|
75
|
+
records that move a site to another server: each site's hostnames with their type,
|
|
76
|
+
TTL (in seconds), and a `◀ here` flag when they point at this server. Connect a
|
|
77
|
+
provider (AWS Route 53 / Cloudflare) and press `⏎` to **edit a record's TTL** — the
|
|
78
|
+
prep step for a low-risk cutover — or `p` to **repoint the record**: pick another
|
|
79
|
+
of your SpinupWP servers from a list (or enter an IP) and move the traffic. It only
|
|
80
|
+
ever touches a site's own hosting records, never your MX/TXT/other records. (See
|
|
81
|
+
"DNS hosts, access & editing" below.)
|
|
82
|
+
- **Database backup & sync** — on a linked WordPress site (Search tab), press `d`
|
|
83
|
+
to download a gzipped production database backup into the copy's `sql/` folder
|
|
84
|
+
(**read-only on production**), or — opt-in — press `p` to pull production into
|
|
85
|
+
your **local** database (backs up local first, rewrites URLs, runs your existing
|
|
86
|
+
`sync.d` hook). Works with Standard WP and Bedrock, no per-site config. (See
|
|
87
|
+
"Database backup & sync" below.)
|
|
88
|
+
- **Production media fallback** — after a DB pull, your local site shows broken
|
|
89
|
+
images because the (often huge) media library isn't synced. Press `m` on a
|
|
90
|
+
linked WordPress site to drop a small mu-plugin that serves any **missing-locally**
|
|
91
|
+
upload from production — so images resolve without copying a single file. Works
|
|
92
|
+
on any local stack (it's pure PHP, not web-server config). (See "Production media
|
|
93
|
+
fallback" below.)
|
|
94
|
+
- **Upgrade a site's PHP version** — press `u` on a site to pick a new PHP
|
|
95
|
+
version and apply it (`PUT /sites/{id}/php`), then watch the upgrade event run
|
|
96
|
+
to completion. (See "Upgrading PHP" below.)
|
|
97
|
+
- **Enable / disable HTTPS** — press `H` on a site to toggle its certificate.
|
|
98
|
+
Enabling issues a free Let's Encrypt cert (the domain must already resolve to
|
|
99
|
+
the server); disabling removes it — the confirm screen warns plainly that
|
|
100
|
+
https:// visitors will see errors until it's re-enabled. Same background
|
|
101
|
+
event-tracking as the PHP upgrade.
|
|
102
|
+
- **Purge cache** — press `P` on a site to clear its page cache and WordPress
|
|
103
|
+
object cache together (SpinupWP has no enable/disable for either on an
|
|
104
|
+
existing site, only purge). Low-risk and fully reversible — both rebuild on
|
|
105
|
+
the next page load.
|
|
106
|
+
- **Server actions** — press `a` on a server to reboot it or restart a service
|
|
107
|
+
(Nginx / PHP-FPM / MySQL / Redis). Servers needing a reboot show a `↻rbt`
|
|
108
|
+
badge, and the overlay reads the box over SSH to show *why* (the pending
|
|
109
|
+
kernel/security packages). (See "Server actions" below.)
|
|
110
|
+
- **Create & connect a server** — press `c` on the Servers tab to provision a new
|
|
111
|
+
server (DigitalOcean / Vultr / Linode / Hetzner), priced from the provider catalog
|
|
112
|
+
and pre-filled to match a selected server. Then press `V` on any server that
|
|
113
|
+
doesn't yet have a **site at its own hostname** to connect it end to end: Spinup
|
|
114
|
+
writes the DNS A record, creates a
|
|
115
|
+
placeholder "vanity" site, enables HTTPS, hands you off to add your SSH key, and
|
|
116
|
+
publishes a status page — turning a bare server into one you can actually work
|
|
117
|
+
with. Long builds run in the background and survive a restart. (See "Creating &
|
|
118
|
+
connecting servers" below.)
|
|
119
|
+
- **Clone a whole server to a new one** — press `C` on a server to clone one or
|
|
120
|
+
more of its sites onto a fresh (or existing) destination, **without touching DNS
|
|
121
|
+
until you say so**. A guided wizard plans + sizes the sites, provisions or picks
|
|
122
|
+
the destination, pulls each site server-to-server (Standard WP **and** Bedrock),
|
|
123
|
+
verifies the clone against the source, and finally repoints DNS on your word.
|
|
124
|
+
Runs in the background with a header badge. (See "Cloning a server" below.)
|
|
125
|
+
- **Privileged writes over SSH (sudo + SSH keys)** — the SpinupWP API can't manage
|
|
126
|
+
SSH keys or sudo users, so Spinup does it directly. Press `S` on a server to
|
|
127
|
+
**connect sudo** for the session (optionally **remembered in your macOS Keychain**
|
|
128
|
+
so it auto-unlocks next time), then `K` on a site to **grant or revoke** your
|
|
129
|
+
personal key and/or Spinup's dedicated machine key — one site or every site on the
|
|
130
|
+
server. (See "Privileged writes over SSH" below.)
|
|
131
|
+
- **Uptime Kuma monitoring** — press `m` on a site to wire it into your own
|
|
132
|
+
[Uptime Kuma](https://github.com/louislam/uptime-kuma) instance (one-time
|
|
133
|
+
connect, 2FA supported, verified by a real login). Vanity pages double as
|
|
134
|
+
health endpoints (`?healthz` returns 503 under CPU pressure / low disk;
|
|
135
|
+
`?format=json&key=…` serves full metrics) and get a load push monitor fed by a
|
|
136
|
+
once-a-minute cron — Kuma graphs server load, and a silent cron acts as a
|
|
137
|
+
dead-man's switch. Monitor status flows back in: a Monitor row in Details, a
|
|
138
|
+
"▼ N monitors down" header badge, uptime/response/load sparklines in the
|
|
139
|
+
Health view, and reboots automatically open a Kuma maintenance window so
|
|
140
|
+
planned downtime never pages you. Regular sites get a homepage monitor
|
|
141
|
+
(up/down + cert-expiry alerts) — client site files are never touched.
|
|
142
|
+
Showed a push URL or health key on a screencast? `r` in the `m` overlay
|
|
143
|
+
**rotates both secrets in place** — the old ones die immediately, the Kuma
|
|
144
|
+
monitor (and its history) survives. Vanity sites are marked `⌂` in the
|
|
145
|
+
Servers tab so they never hide among domain-named servers' sites.
|
|
146
|
+
(See `docs/uptime-kuma.md` for recipes.)
|
|
147
|
+
- **Site monitoring: fingerprints, a doctor, and a Redis sentinel** — press `M`
|
|
148
|
+
on any site (Servers / Stacks / Search) for the monitoring overlay. `f`
|
|
149
|
+
calibrates a **front-page check**: Spinup reads your healthy front page,
|
|
150
|
+
derives a template-identity fingerprint from what WordPress itself stamps
|
|
151
|
+
into the markup (not a headline someone might edit), proves it discriminates,
|
|
152
|
+
and registers a Kuma keyword monitor at your chosen window (5m–1h) — catching
|
|
153
|
+
"the cache is serving the wrong page while HTTP stays 200". `d` runs the
|
|
154
|
+
**site doctor**: a read-only cache differential (cached vs. bypassed render,
|
|
155
|
+
proven by the server's own `fastcgi-cache` headers) plus a wp-admin door
|
|
156
|
+
probe, diagnosing stale caches and **partial outages** — cached pages fine
|
|
157
|
+
while everything uncached throws 500 (a dead Redis does exactly this) — and
|
|
158
|
+
ending in a copyable runbook. `n` wires **alerts**: Spinup detects the
|
|
159
|
+
notification providers configured in Kuma by name and toggles them per site.
|
|
160
|
+
And servers get a **Redis sentinel**: the heartbeat cron pings Redis every
|
|
161
|
+
minute and reports up/down to its own `{server} redis` monitor.
|
|
162
|
+
(See "Site monitoring" below.)
|
|
163
|
+
- **Completion toasts** — background writes that take a while (a PHP upgrade, a
|
|
164
|
+
server reboot/restart, resolving your fleet's DNS) raise a non-focus-stealing
|
|
165
|
+
toast when they finish, so you're not left guessing after you've moved on.
|
|
166
|
+
- **Release notes** — after Spinup updates to a new version, a one-time overlay
|
|
167
|
+
shows what changed (sourced straight from that version's GitHub release —
|
|
168
|
+
no separate feed to maintain). Press `n` in `?` Help to see the current
|
|
169
|
+
version's notes again any time.
|
|
170
|
+
|
|
171
|
+
> The tool is **read-only by default** and works great with a Read Only API
|
|
172
|
+
> token. The write actions — creating a server, connecting it with a vanity site,
|
|
173
|
+
> cloning a server, upgrading a site's PHP version, and rebooting / restarting
|
|
174
|
+
> services — need a **Read/Write** token. The SSH actions (sudo connect, grant key,
|
|
175
|
+
> clone pull) use **your own SSH access**, not the API token; everything else keeps
|
|
176
|
+
> working without any of it.
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- [Bun](https://bun.sh) ≥ 1.3 (OpenTUI uses Bun's native FFI). Install with:
|
|
181
|
+
```sh
|
|
182
|
+
curl -fsSL https://bun.sh/install | bash
|
|
183
|
+
```
|
|
184
|
+
- A SpinupWP API token — create one at
|
|
185
|
+
[spinupwp.app/account/api](https://spinupwp.app/account/api/). **Read Only**
|
|
186
|
+
scope is enough to browse; use **Read/Write** if you want to upgrade a site's
|
|
187
|
+
PHP version.
|
|
188
|
+
|
|
189
|
+
## Install & run
|
|
190
|
+
|
|
191
|
+
The easy way — install the published package globally:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
bun install -g spinuptui
|
|
195
|
+
spinuptui login # save your API token to the config file (once)
|
|
196
|
+
spinuptui # launch from any directory
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
On first launch, if no token is configured you'll be guided through a short
|
|
200
|
+
onboarding flow that validates your token and saves it locally.
|
|
201
|
+
|
|
202
|
+
### From source
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
git clone <this-repo> spinuptui
|
|
206
|
+
cd spinuptui
|
|
207
|
+
bun install
|
|
208
|
+
bun run start
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
To run a source checkout from anywhere, install the `spinuptui` command globally
|
|
212
|
+
with a symlink to the checkout (updates as you pull):
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
bun run link-global # = bun link; creates `spinuptui` on your PATH
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`spinuptui login` is what makes it work outside the project: the project `.env`
|
|
219
|
+
is only read from the project directory, so the global command relies on the
|
|
220
|
+
token saved in the config file. (Run `bun run unlink-global` to remove the
|
|
221
|
+
command.)
|
|
222
|
+
|
|
223
|
+
For a standalone binary that doesn't need Bun on `PATH` at runtime:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
bun run build:binary # produces ./spinuptui — move it onto your PATH
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Updating
|
|
230
|
+
|
|
231
|
+
The app tells you when a newer release exists — a gold `✦ vX.Y.Z` appears next
|
|
232
|
+
to the version in the header (and in the `?` About panel).
|
|
233
|
+
|
|
234
|
+
- **Package install:** `bun update -g spinuptui` (the About panel shows the
|
|
235
|
+
same command).
|
|
236
|
+
- **Source checkout:** press **`u`** in the About panel to update in place
|
|
237
|
+
(`git pull --ff-only`; refuses if you have uncommitted changes, and never
|
|
238
|
+
merges/rebases). It can't hot-reload the already-running process, so it tells
|
|
239
|
+
you plainly when to restart — press `q`, then relaunch `spinuptui`. **If the
|
|
240
|
+
update changed dependencies, it tells you to run `bun install` too** before
|
|
241
|
+
restarting. `git pull` by hand works the same way — the global `spinuptui`
|
|
242
|
+
symlink picks up the new code immediately. A standalone binary needs a fresh
|
|
243
|
+
`bun run build:binary` either way.
|
|
244
|
+
|
|
245
|
+
#### CLI subcommands
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
spinuptui Launch the dashboard
|
|
249
|
+
spinuptui login Set or update your saved API token
|
|
250
|
+
spinuptui where Show the config path and which source the token came from
|
|
251
|
+
spinuptui --version Print the version
|
|
252
|
+
spinuptui --help Show help
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Configuration
|
|
256
|
+
|
|
257
|
+
The token is resolved in this order (first match wins):
|
|
258
|
+
|
|
259
|
+
1. **`SPINUPWP_ACCESS_TOKEN`** environment variable. Bun automatically loads a
|
|
260
|
+
`.env` file from the working directory, so a project-local `.env` works:
|
|
261
|
+
```sh
|
|
262
|
+
# .env
|
|
263
|
+
SPINUPWP_ACCESS_TOKEN=your-token-here
|
|
264
|
+
```
|
|
265
|
+
2. **`~/.config/spinupwp-tui/config.json`** — written by the onboarding wizard.
|
|
266
|
+
Respects `XDG_CONFIG_HOME`.
|
|
267
|
+
|
|
268
|
+
To reconfigure, delete the config file (the path is shown on the onboarding
|
|
269
|
+
screen) and relaunch, or set the environment variable.
|
|
270
|
+
|
|
271
|
+
### Optional settings
|
|
272
|
+
|
|
273
|
+
These can be set in `config.json` or via an environment variable:
|
|
274
|
+
|
|
275
|
+
- **`accountSlug`** / `SPINUPWP_ACCOUNT_SLUG` — your SpinupWP account/team slug
|
|
276
|
+
(the first path segment in a SpinupWP URL, e.g. `wenmark-digital-solutions` in
|
|
277
|
+
`https://spinupwp.app/wenmark-digital-solutions/servers/35633`). The API
|
|
278
|
+
doesn't expose it, so set it to enable the `w` deep links into the web app.
|
|
279
|
+
Without it, `w` opens the SpinupWP dashboard root.
|
|
280
|
+
- **`sshUser`** / `SPINUPWP_SSH_USER` — override the SSH user for the health view
|
|
281
|
+
and stack probes (see "Server health" below).
|
|
282
|
+
- **`localSync`** / `SPINUPWP_LOCAL_SYNC` — opt-in for the **Pull production →
|
|
283
|
+
local** DB sync (`p`); off by default because it overwrites your local database.
|
|
284
|
+
**Prefer `"localSync": true` in `config.json`** — it's read from a fixed path
|
|
285
|
+
(`~/.config/spinupwp-tui/config.json`) so it applies wherever you launch `spinuptui`
|
|
286
|
+
from. The environment variable works too, but note a `.env` is only loaded when
|
|
287
|
+
you launch from the directory that contains it (Bun reads `.env` from the current
|
|
288
|
+
working directory, not from where the installed command lives) — so a repo or
|
|
289
|
+
project `.env` won't take effect for the globally-installed `spinuptui` run from
|
|
290
|
+
elsewhere. For a persistent, location-independent setting, use `config.json`.
|
|
291
|
+
|
|
292
|
+
- **`uptimeKuma`** / `SPINUP_KUMA_URL` + `SPINUP_KUMA_USERNAME` +
|
|
293
|
+
`SPINUP_KUMA_PASSWORD` — an Uptime Kuma connection for the monitoring
|
|
294
|
+
features. Easiest path: press `m` on any site and connect in-app (creds are
|
|
295
|
+
verified by logging in, then stored in `config.json`, chmod 600, alongside a
|
|
296
|
+
login token so 2FA is only asked once). The env trio exists for
|
|
297
|
+
externally-managed setups and is read-only in the UI.
|
|
298
|
+
|
|
299
|
+
## Keybindings
|
|
300
|
+
|
|
301
|
+
| Key | Action |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| `1`…`5` | Switch tabs: Dashboard · Servers · Stacks · Search · Events |
|
|
304
|
+
| `↑`/`↓` or `j`/`k` | Move selection |
|
|
305
|
+
| `Enter` / `→` | Drill in (server → its sites) |
|
|
306
|
+
| `←` / `Esc` | Go back / collapse |
|
|
307
|
+
| `Tab` | Switch focus between columns |
|
|
308
|
+
| `g` / `G` | Jump to top / bottom |
|
|
309
|
+
| `o` | Open the selected site in your browser |
|
|
310
|
+
| `s` | Open a terminal and SSH into the selected site |
|
|
311
|
+
| `d` | Download a production DB backup into the linked copy (Search; WordPress + linked) |
|
|
312
|
+
| `p` | Pull the production DB into the linked copy — overwrites local; opt-in via `localSync` (Search) |
|
|
313
|
+
| `m` | Production media fallback: serve missing-locally images from production (Search; WordPress + linked) |
|
|
314
|
+
| `L` | Link / edit a site's local working copy |
|
|
315
|
+
| `t` / `v` | Open the linked copy in a terminal / its local URL in your browser |
|
|
316
|
+
| `n` | DNS migration view for a site — its records + TTLs (`⏎` edits a TTL; `p` repoints the record; `a` shows the whole server) |
|
|
317
|
+
| `N` | DNS migration view for the whole server |
|
|
318
|
+
| `h` | Live server health (CPU/mem/disk over SSH) |
|
|
319
|
+
| `p` | List a site's installed plugins & themes over SSH — version + updates (Servers tab, sites pane) |
|
|
320
|
+
| `d` | Detect a site's stack via SSH (Servers / Stacks tabs) |
|
|
321
|
+
| `D` | Detect every site in the selected stack (Stacks tab) |
|
|
322
|
+
| `S` | Auto-discover & batch-link local copies (Stacks tab) |
|
|
323
|
+
| `f` | Report sites with no usable local copy (Stacks tab) |
|
|
324
|
+
| `u` | Upgrade a site's PHP version (Servers / Stacks / Search; needs a Read/Write token) |
|
|
325
|
+
| `H` | Enable / disable HTTPS on a site (Servers / Stacks / Search; needs a Read/Write token) |
|
|
326
|
+
| `P` | Purge a site's page cache + object cache (Servers / Stacks / Search; needs a Read/Write token) |
|
|
327
|
+
| `m` | Site monitoring via Uptime Kuma (Servers tab, sites pane — same overlay as `M`) |
|
|
328
|
+
| `M` | Site monitoring overlay — add monitors, `f` front-page check, `d` site doctor, `n` alert wiring, vanity refresh/rotation (Servers / Stacks / Search) |
|
|
329
|
+
| `a` | Server actions: reboot / restart a service (Servers / Search; needs a Read/Write token) |
|
|
330
|
+
| `c` | Create a new server (Servers tab; needs a Read/Write token) |
|
|
331
|
+
| `V` | Add a vanity site at the server's own hostname — DNS + site + HTTPS + SSH-key handoff (Servers tab; offered when no hostname site exists; needs a Read/Write token) |
|
|
332
|
+
| `C` | Clone a server's sites to a new/existing destination (Servers tab; needs a Read/Write token + sudo) |
|
|
333
|
+
| `S` | Connect sudo on a server for privileged writes — optionally remembered in the macOS Keychain (Servers tab) |
|
|
334
|
+
| `K` | Grant / revoke an SSH key on a site, or every site on the server (needs sudo connected) |
|
|
335
|
+
| `w` | Open the selected server/site in the SpinupWP web app |
|
|
336
|
+
| `/` | Jump to global search |
|
|
337
|
+
| `r` | Refresh data from the API |
|
|
338
|
+
| `i` | Explain the current screen (what each pane and key does) |
|
|
339
|
+
| `?` | Toggle the help overlay |
|
|
340
|
+
| `q` / `Ctrl+C` | Quit |
|
|
341
|
+
|
|
342
|
+
In the **Search** tab the box keeps keyboard focus while you type. Press **Tab**
|
|
343
|
+
(or **→**) to hand focus to the selected result's **action menu** — `o` / `w` /
|
|
344
|
+
`u` / `h` then act on that server or site — and **←** / **Esc** to return to the
|
|
345
|
+
search box.
|
|
346
|
+
|
|
347
|
+
## Server health (SSH)
|
|
348
|
+
|
|
349
|
+
The SpinupWP API exposes no live metrics, so the health view (`h` in the
|
|
350
|
+
Servers tab) reaches the server directly over SSH using **your local SSH keys /
|
|
351
|
+
agent** — the same way you'd `ssh in` and run `htop`. It runs a single batched,
|
|
352
|
+
**read-only** command (`cat /proc/*`, `df`, `ps`) and renders the result.
|
|
353
|
+
|
|
354
|
+
- **Connection target** is derived from the API: it connects as one of the
|
|
355
|
+
server's `site_user`s at the server's IP (`site_user@ip`). No extra config
|
|
356
|
+
needed if `ssh site_user@ip` already works from your terminal.
|
|
357
|
+
- **Non-interactive:** it uses `BatchMode=yes`, so if key auth isn't already set
|
|
358
|
+
up it fails fast with a hint rather than prompting for a password.
|
|
359
|
+
- **Override the SSH user** (e.g. to use `root` or a sudo user) with the
|
|
360
|
+
`SPINUPWP_SSH_USER` environment variable, or `"sshUser"` in the config file.
|
|
361
|
+
- A persistent `ControlMaster` connection keeps repeated polls fast.
|
|
362
|
+
|
|
363
|
+
Nothing is ever written to the server.
|
|
364
|
+
|
|
365
|
+
## Stack detection
|
|
366
|
+
|
|
367
|
+
The **Stacks** tab (`3`) breaks your fleet into buckets and helps you see what's
|
|
368
|
+
actually running where. It works in two tiers:
|
|
369
|
+
|
|
370
|
+
- **Tier 1 — instant, no SSH.** Every site is classified from data the API
|
|
371
|
+
already returns: **Non-WP**, **Bedrock** (WordPress with a `/web/` webroot), or
|
|
372
|
+
**Standard WP**. The left pane shows counts and bars; the right pane shows the
|
|
373
|
+
fleet-wide **PHP version distribution** with end-of-life versions flagged.
|
|
374
|
+
|
|
375
|
+
- **Tier 2 — on-demand SSH probe.** Press `d` on a site (in the Stacks or
|
|
376
|
+
Servers tab) to inspect its filesystem **read-only** and identify it precisely:
|
|
377
|
+
**WordPress** (with version), **Bedrock**, **WHMCS**, **Laravel**, or
|
|
378
|
+
**Static HTML**. Press `D` to probe an entire stack in list order (bounded SSH
|
|
379
|
+
concurrency). A conclusive probe **overrides** the Tier-1 guess — so a site the
|
|
380
|
+
API mislabels (e.g. WordPress installed outside SpinupWP's installer reports
|
|
381
|
+
`is_wordpress=false`) moves into its true bucket. The Non-WP bucket expands
|
|
382
|
+
into named sub-rows (WHMCS / Laravel / Static HTML / Unknown / unprobed).
|
|
383
|
+
|
|
384
|
+
Probes reuse the same SSH access as the health view (`site_user@ip`, your local
|
|
385
|
+
keys, `BatchMode`) and are **read-only**. Results are cached to
|
|
386
|
+
`~/.config/spinupwp-tui/stack-cache.json`, hydrated at startup, so detections
|
|
387
|
+
survive restarts without re-running SSH.
|
|
388
|
+
|
|
389
|
+
## Installed plugins & themes
|
|
390
|
+
|
|
391
|
+
Press `p` on a site (Servers tab, sites pane) to list its installed plugins and
|
|
392
|
+
themes over SSH — the `wp plugin list` / `wp theme list` detail the SpinupWP API
|
|
393
|
+
never exposes (it only gives you *counts* of pending updates). You see every
|
|
394
|
+
plugin and theme with its **status** (active / inactive / must-use / dropin),
|
|
395
|
+
**current version**, and the **new version** when an update is available (`→ 1.2.3`
|
|
396
|
+
in gold; `✓ current` otherwise), grouped into `PLUGINS` and `THEMES` sections each
|
|
397
|
+
with an update badge.
|
|
398
|
+
|
|
399
|
+
It's strictly **read-only**: it runs wp-cli as the site user using your local SSH
|
|
400
|
+
keys (the same non-interactive auth the health view uses), and **detects the real
|
|
401
|
+
WordPress directory itself** rather than trusting the `public_folder` setting — so
|
|
402
|
+
it works on standard `/public/` installs and Bedrock (`web/wp`) alike, and even on
|
|
403
|
+
sites SpinupWP misclassifies as non-WordPress (it finds WordPress core over SSH).
|
|
404
|
+
Non-WordPress sites get a clear "no WordPress core found" message.
|
|
405
|
+
|
|
406
|
+
`↑↓` / `jk` scroll, `r` re-reads over SSH, `Esc` / `q` / `p` closes.
|
|
407
|
+
|
|
408
|
+
## Upgrading PHP
|
|
409
|
+
|
|
410
|
+
Press `u` on a selected site (in the **Servers** or **Stacks** tab) to change its
|
|
411
|
+
PHP version. A picker lists the available versions — the current one is marked,
|
|
412
|
+
end-of-life versions are flagged, and the list is sourced from the live PHP
|
|
413
|
+
release schedule (so new versions like 8.5 appear automatically). After you
|
|
414
|
+
confirm, the app calls `PUT /sites/{id}/php` and polls the resulting event until
|
|
415
|
+
it finishes.
|
|
416
|
+
|
|
417
|
+
- **Needs a Read/Write token.** SpinupWP exposes no token-scope endpoint, so a
|
|
418
|
+
read-only token is detected when the upgrade comes back `403` — you'll get a
|
|
419
|
+
clear "token is read-only" message and nothing changes. Swap in a Read/Write
|
|
420
|
+
token (`spinuptui login`) to actually apply upgrades.
|
|
421
|
+
- **On-demand install.** If the chosen version isn't installed on the server yet,
|
|
422
|
+
SpinupWP installs it first; the event simply takes a little longer.
|
|
423
|
+
- **Pending platform upgrade.** If the site's server has a pending SpinupWP
|
|
424
|
+
platform upgrade, it can't be managed via the API until that runs — the picker
|
|
425
|
+
is blocked and points you to open the server in the web app (`w`).
|
|
426
|
+
- **Runs in the background.** The upgrade is tracked in the app's store, so you
|
|
427
|
+
can press `Esc` to close the modal and it keeps going — the site's row shows a
|
|
428
|
+
spinner and the target version (`→8.3`) until it settles, then refreshes to the
|
|
429
|
+
new version (or flags `⬆!` if it failed). The SiteDetail "PHP" field shows the
|
|
430
|
+
same in-progress state. You can launch upgrades on several sites at once. When an
|
|
431
|
+
upgrade finishes, a **toast** confirms it (`example.com upgraded to PHP 8.3`) —
|
|
432
|
+
useful since it often completes after you've closed the modal and moved on.
|
|
433
|
+
|
|
434
|
+
## Server actions
|
|
435
|
+
|
|
436
|
+
Press `a` on a selected server (in the **Servers** tab, or a result in **Search**)
|
|
437
|
+
to open the server-actions overlay: **reboot** the server, or **restart** a single
|
|
438
|
+
service (Nginx / PHP-FPM / MySQL / Redis). Pick → confirm → the app calls
|
|
439
|
+
`POST /servers/{id}/reboot` or `/services/{svc}/restart` and tracks the event to
|
|
440
|
+
completion — same background behavior as PHP upgrades (close the overlay and the
|
|
441
|
+
server's row keeps a spinner).
|
|
442
|
+
|
|
443
|
+
- **Needs a Read/Write token** (like PHP upgrades).
|
|
444
|
+
- **Reboot visibility.** Servers with a pending reboot show a `↻rbt` badge in the
|
|
445
|
+
Servers list and on the Dashboard's "Needs attention" panel.
|
|
446
|
+
- **Why a reboot is pending.** The API only exposes a `reboot_required` boolean —
|
|
447
|
+
no reason. So when you open the overlay on a flagged server, the app reads
|
|
448
|
+
Ubuntu's `/var/run/reboot-required` + `.pkgs` over SSH (read-only, reusing the
|
|
449
|
+
health view's connection) and shows the pending packages — typically a
|
|
450
|
+
**kernel/security update**. This is labeled as OS-level context, not as
|
|
451
|
+
SpinupWP's internal logic (a fleet-wide check confirmed the boolean tracks that
|
|
452
|
+
file 1:1).
|
|
453
|
+
- **Reboot is the big one** — its confirmation calls out that it takes the whole
|
|
454
|
+
server down briefly (every site on it); a service restart is a brief blip.
|
|
455
|
+
- **A toast on completion.** A reboot can take minutes; when it (or a restart)
|
|
456
|
+
finishes, a non-focus-stealing toast tells you (`example.com rebooted` /
|
|
457
|
+
`Nginx restarted on example.com`), so you don't have to keep checking.
|
|
458
|
+
|
|
459
|
+
## Site monitoring
|
|
460
|
+
|
|
461
|
+
Everything hangs off one overlay: press `M` on a site in any view (the Servers
|
|
462
|
+
tab also keeps its original `m`). It talks to your own
|
|
463
|
+
[Uptime Kuma](https://github.com/louislam/uptime-kuma) — Kuma watches 24/7 and
|
|
464
|
+
sends the alerts; Spinup creates the monitors *correctly* and diagnoses on
|
|
465
|
+
demand.
|
|
466
|
+
|
|
467
|
+
- **`a` — uptime monitors.** Regular sites get a homepage monitor (up/down +
|
|
468
|
+
cert-expiry). Vanity sites get the full server treatment: healthz + load
|
|
469
|
+
push + a **Redis sentinel** — the heartbeat cron runs `redis-cli ping` every
|
|
470
|
+
minute and reports up/down to a `{server} redis` monitor (registered only
|
|
471
|
+
after probing that Redis actually answers on that box). Why it matters: with
|
|
472
|
+
SpinupWP's default object-cache drop-in, a dead Redis is **fatal (HTTP 500)
|
|
473
|
+
for every request that misses the page cache** — cached pages keep serving
|
|
474
|
+
200, so only the sentinel notices, within a minute.
|
|
475
|
+
- **`f` — the front-page check.** Spinup reads your *healthy* front page,
|
|
476
|
+
derives a template-identity fingerprint from WordPress's own `body_class()`
|
|
477
|
+
output (`class="home`, `page-id-N`, …, falling back to the canonical link),
|
|
478
|
+
validates it against a throwaway search render to prove it discriminates,
|
|
479
|
+
and registers a Kuma keyword monitor at your chosen window (5m default —
|
|
480
|
+
the check is served straight from the page cache, so it costs the site
|
|
481
|
+
nothing). A red check means "answering 200 but serving the WRONG page" —
|
|
482
|
+
the stale/corrupt-page-cache incident — and shows as `WRONG PAGE SERVED` in
|
|
483
|
+
Details. Re-running `f` recalibrates the same monitor in place (history
|
|
484
|
+
survives a redesign).
|
|
485
|
+
- **`d` — the site doctor.** Read-only, zero setup, works without Kuma. It
|
|
486
|
+
fetches the page twice — once normally, once with the cache-bypass cookie —
|
|
487
|
+
and lets the server's `fastcgi-cache: HIT/BYPASS` headers prove which layer
|
|
488
|
+
answered each request, then compares template identity between the two. It
|
|
489
|
+
also knocks on the wp-admin door (the login page — never cached; Bedrock's
|
|
490
|
+
relocated `/wp/` login handled). Verdicts: healthy · stale-cache (with a
|
|
491
|
+
copyable runbook; the Elementor one-command flush is suggested only when
|
|
492
|
+
Elementor is detected in the served markup) · **partial-outage** (cached
|
|
493
|
+
pages 200, fresh renders or wp-admin 5xx — Redis or a PHP fatal; visitors
|
|
494
|
+
look fine, admins are broken) · recalibrate (`f` jumps straight there) ·
|
|
495
|
+
down · inconclusive.
|
|
496
|
+
- **`n` — alert wiring.** Spinup detects the notification providers configured
|
|
497
|
+
in your Kuma (by name — Telegram, email, whatever you've set up) and shows
|
|
498
|
+
whether each is attached to this site's monitors (`✓` all / `◐` some / `○`
|
|
499
|
+
none); `⏎` toggles a provider across all of the site's checks at once.
|
|
500
|
+
Creating the provider (bot tokens etc.) is the one step that stays in Kuma.
|
|
501
|
+
|
|
502
|
+
The design principle across all of it: **the page cache makes sites look
|
|
503
|
+
healthy while things break behind it** — every check watches either *through*
|
|
504
|
+
the cache on purpose (is the right thing being served?) or deliberately
|
|
505
|
+
*around* it (is the machinery behind it alive?).
|
|
506
|
+
|
|
507
|
+
## Creating & connecting servers
|
|
508
|
+
|
|
509
|
+
Two write actions on the **Servers** tab that stand up a new server and make it
|
|
510
|
+
usable — both **need a Read/Write token**.
|
|
511
|
+
|
|
512
|
+
**Create a server (`c`).** Opens a form pre-filled to **match** the selected
|
|
513
|
+
server's provider, region, and size (or from scratch on an empty fleet). It prices
|
|
514
|
+
the build from the provider's catalog (DigitalOcean / Vultr / Linode / Hetzner),
|
|
515
|
+
suggests a hostname from your fleet's naming convention, and lets you switch
|
|
516
|
+
provider (`p`), region (`g`), or size (`e`) and toggle backups (`b`) before
|
|
517
|
+
confirming. Because the SpinupWP API can't list your configured server providers,
|
|
518
|
+
the first time you build on a provider the overlay asks for its SpinupWP provider
|
|
519
|
+
id (Account Settings → Server Providers) and saves it — once per provider. The
|
|
520
|
+
build fires `POST /servers` and tracks the ~10-minute provision in the background.
|
|
521
|
+
|
|
522
|
+
**Connect it with a vanity site (`V`).** A brand-new server has **no site**, so
|
|
523
|
+
there's nothing to attach an SSH key to and no way for Spinup to reach it — empty
|
|
524
|
+
servers are flagged in **amber** in the Servers list. A busy server benefits from
|
|
525
|
+
the same thing (a status page at its own hostname + a site user to hold your key),
|
|
526
|
+
so `V` is offered on **any server that doesn't yet have a site at its own
|
|
527
|
+
hostname** — it also appears under **Manage** in the server's Details panel. It
|
|
528
|
+
builds the small placeholder ("vanity") site end to end:
|
|
529
|
+
|
|
530
|
+
1. **DNS** — writes an `A` record for the hostname → the server IP (AWS Route 53),
|
|
531
|
+
using the connection from the DNS module.
|
|
532
|
+
2. **Propagate** — waits for the record to resolve (so Let's Encrypt can issue);
|
|
533
|
+
after ~2 minutes it offers to skip SSL for now or keep waiting.
|
|
534
|
+
3. **Site** — creates a blank site (`installation_method: "blank"`).
|
|
535
|
+
4. **HTTPS** — enables a free Let's Encrypt certificate.
|
|
536
|
+
5. **SSH key** — deep-links you to the site's **SFTP & SSH → Site User** to add
|
|
537
|
+
your key (the API can't do this), then waits for you to confirm.
|
|
538
|
+
6. **Publish** — seeds a minimal, brand-neutral status page (it reads its own
|
|
539
|
+
hostname, so it's reusable on any server). Press `o` to open the live site.
|
|
540
|
+
|
|
541
|
+
The whole build is a **resumable background job**: close the overlay and a header
|
|
542
|
+
badge keeps tracking it; press `V` on the server to reopen it (even after the site
|
|
543
|
+
exists). It survives quitting and relaunching the app. If a step fails, the overlay
|
|
544
|
+
shows where, with `r` to retry or `x` to discard.
|
|
545
|
+
|
|
546
|
+
## Privileged writes over SSH (sudo & SSH keys)
|
|
547
|
+
|
|
548
|
+
Some things the SpinupWP API simply can't do — it has **no** surface for SSH keys or
|
|
549
|
+
sudo users. Spinup does them directly over SSH instead, which means these actions use
|
|
550
|
+
**your own SSH access**, not the API token.
|
|
551
|
+
|
|
552
|
+
**Connect sudo (`S`).** Press `S` on a server and enter its SpinupWP **sudo user**
|
|
553
|
+
and that user's **sudo password** once. Spinup validates them against the live server
|
|
554
|
+
(`sudo -S -p '' true`) and then holds the password **in memory for the session only**
|
|
555
|
+
— the username persists to config, the password is never written to plaintext config.
|
|
556
|
+
A connected server shows a green `● sudo` badge on its row; `S` again disconnects.
|
|
557
|
+
|
|
558
|
+
- **Remember in the macOS Keychain (opt-in).** Tick the toggle when connecting and the
|
|
559
|
+
password is saved to your **login Keychain** (service `spinup-sudo`, one item per
|
|
560
|
+
server) — never to `config.json`, which only keeps the username and a `keychain`
|
|
561
|
+
marker. Next time you press `S` on that server, sudo **auto-unlocks** with no
|
|
562
|
+
retyping (the first read may show macOS's own "allow access" prompt — choose Always
|
|
563
|
+
Allow). Press `f` to forget the saved password; disconnecting (`x`) a saved server
|
|
564
|
+
offers a no-password **reconnect** rather than the credential form. Off macOS the
|
|
565
|
+
toggle is absent and sudo stays in-memory per session.
|
|
566
|
+
|
|
567
|
+
**Grant / revoke an SSH key (`K`).** With sudo connected, press `K` on a site to write
|
|
568
|
+
keys into the site user's `authorized_keys`:
|
|
569
|
+
|
|
570
|
+
- **Pick which keys** — any of **your personal keys** (discovered from `~/.ssh/*.pub`
|
|
571
|
+
and your ssh-agent, so you can SSH/SFTP as yourself) and/or Spinup's dedicated
|
|
572
|
+
**`spinup-tui` machine key** (an ed25519 identity generated once into the config dir,
|
|
573
|
+
deliberately **never added to your SpinupWP account** so SpinupWP's key reconciliation
|
|
574
|
+
can't clobber it). Your selection is remembered.
|
|
575
|
+
- **Pick the scope** — just this site, or **every site on the server** in one pass
|
|
576
|
+
(per-site progress, with a retry for any that fail).
|
|
577
|
+
- **Grant or remove** — `a`/`r` toggles the mode; removing pulls exactly the chosen
|
|
578
|
+
key lines and leaves every other key (including SpinupWP-managed ones) untouched. The
|
|
579
|
+
remote script is **idempotent** and a confirm overlay shows the exact command first.
|
|
580
|
+
- Site rows show what's granted at a glance — **👤** (your key) and/or **🔑** (the
|
|
581
|
+
machine key).
|
|
582
|
+
|
|
583
|
+
## Cloning a server
|
|
584
|
+
|
|
585
|
+
Press **`C`** on a server to clone one or more of its sites onto a **new or existing**
|
|
586
|
+
destination server — a guided, two-pane wizard that lets you stage and verify a whole
|
|
587
|
+
migration and **only repoints DNS when you say so**. It needs a **Read/Write token**
|
|
588
|
+
(to create the destination sites) and **sudo connected on both ends** (the copy runs
|
|
589
|
+
over SSH). The steps:
|
|
590
|
+
|
|
591
|
+
1. **Plan** — pick which of the source's sites to clone (all selected by default;
|
|
592
|
+
`space` toggles). Spinup sizes each one live (disk + database) into a payload total
|
|
593
|
+
so you know what you're moving; a concurrency cap protects the busy source.
|
|
594
|
+
2. **Destination** — provision a fresh server pre-matched to the source (reusing the
|
|
595
|
+
`c` flow), or `d` to pick an existing server as the target.
|
|
596
|
+
3. **Connect** — connect sudo on **both** servers. The clone is a server-to-server
|
|
597
|
+
**pull**: the destination pulls each site directly from the source over SSH (no
|
|
598
|
+
bytes routed through your laptop), authenticating with a key granted onto the source
|
|
599
|
+
for the job and **revoked when it's done**.
|
|
600
|
+
4. **Git access** (only when a Bedrock site is selected) — each repo gets its **own
|
|
601
|
+
read-only deploy key**, generated locally and never persisted: the public half
|
|
602
|
+
goes on the repo (added for you via `gh` when it's installed and authed, or shown
|
|
603
|
+
for a manual add — `o` opens the repo's deploy-key settings, `y` copies the key),
|
|
604
|
+
and the pair rides the site create so SpinupWP installs it as the new site's git
|
|
605
|
+
identity. Per-site keys are what let any number of Bedrock repos land on one
|
|
606
|
+
server — GitHub allows a deploy key on only **one** repository account-wide, so a
|
|
607
|
+
shared server key stops working at the second repo.
|
|
608
|
+
5. **Clone sites** — a live roster runs the sites concurrently, each advancing
|
|
609
|
+
`create → pull → config → verify → done` with **live transfer progress** (bytes,
|
|
610
|
+
rate, elapsed; database pulls show a true percent). Three stacks are handled:
|
|
611
|
+
**Standard WP** (files + database, with `wp-config` re-stamped for the
|
|
612
|
+
destination), **Bedrock** (git-native — created from the repo, `composer install`
|
|
613
|
+
over SSH, uploads + secrets pulled, `.env` re-stamped), and **files-only** for
|
|
614
|
+
non-WordPress sites (redirect shells, static/PHP — opt-in in Plan, no database).
|
|
615
|
+
The pull **detects each site's real webroot** rather than trusting settings —
|
|
616
|
+
`public/`-style layouts (with `wp-config.php` one level above the webroot) are
|
|
617
|
+
preserved, and mid-move layouts are normalized on the destination. **Additional
|
|
618
|
+
domains carry over** automatically (with their redirect settings), so the clone
|
|
619
|
+
answers for every hostname the source did.
|
|
620
|
+
6. **Verify** — drill into any cloned site for a source-vs-clone comparison (wp-cli
|
|
621
|
+
facts + an HTTP check that hits the **new** server while DNS still points at the old
|
|
622
|
+
one; files-only sites compare file count, size, and HTTP instead).
|
|
623
|
+
7. **DNS cutover** — the wizard **waits for your explicit go** (`c`) after the roster
|
|
624
|
+
settles, then repoints `A` records across each site's domains (apex + additional)
|
|
625
|
+
to the new server in one batched, partial-aware pass; `↑↓`/`space` include or
|
|
626
|
+
exclude individual records first. `www`-style records that follow the apex are
|
|
627
|
+
skipped, not clobbered. Cloudflare **proxied** records repoint automatically too
|
|
628
|
+
(their origin IP is always PATCHable even though their TTL stays fixed to
|
|
629
|
+
automatic). Records in zones you can't edit show your DNS access note and `⏎`
|
|
630
|
+
opens that zone's registrar console with the zone name copied — ready to paste.
|
|
631
|
+
|
|
632
|
+
The clone runs in the **background** — pressing `Esc` doesn't abandon it; a header
|
|
633
|
+
badge (`⠹ Cloning … — press C`) surfaces the in-flight job and `C` reopens the live
|
|
634
|
+
roster. `←` (or `h`) steps **back a screen**: the setup steps go back freely, and
|
|
635
|
+
once cloning has started the one back edge is DNS cutover → the clone roster — so
|
|
636
|
+
you can re-verify or retry a site before flipping live traffic. **Every job writes a full log** (`~/.config/spinupwp-tui/logs/`, passwords
|
|
637
|
+
redacted, self-pruning) — `⏎` on a failed site shows the complete error and `r`
|
|
638
|
+
retries just that site. Pairs naturally with the DNS module: **lower the TTLs**
|
|
639
|
+
(`n`/`N` → `⏎`) a day or two ahead so the cutover propagates fast.
|
|
640
|
+
|
|
641
|
+
## Local working copies
|
|
642
|
+
|
|
643
|
+
Bridge your SpinupWP sites to the local checkouts you actually edit. Press `L` on
|
|
644
|
+
a site (Servers / Stacks / Search) to link a path and the local URL where you
|
|
645
|
+
serve it; the site's details gain a "Local" field, and you can open the copy with
|
|
646
|
+
`t` (a terminal at the path) or `v` (its local URL). All of this is **local-only**
|
|
647
|
+
— no SpinupWP writes.
|
|
648
|
+
|
|
649
|
+
- **Auto-discover (`S`, Stacks tab).** Scan one or more folders and match their
|
|
650
|
+
subdirectories to sites — by git remote, Bedrock `WP_HOME`, or folder name —
|
|
651
|
+
then batch-link the matches.
|
|
652
|
+
- **"Needs a local copy" report (`f`, Stacks tab).** Lists the managed sites you
|
|
653
|
+
have no usable local copy for (never linked, or a missing path), filterable by
|
|
654
|
+
stack.
|
|
655
|
+
- **Markers & drift.** Linked sites show `◆` in the lists; a linked, on-disk copy
|
|
656
|
+
shows its local git drift (`⇡N unpushed` / `● uncommitted`), read from the repo
|
|
657
|
+
with no network.
|
|
658
|
+
|
|
659
|
+
Config keys: `localRoots` (folders to scan) and `localSites` (per-site path +
|
|
660
|
+
local URL — tool-agnostic: Valet, Cove, LocalWP, Herd, DDEV, …).
|
|
661
|
+
|
|
662
|
+
## Database backup & sync
|
|
663
|
+
|
|
664
|
+
For a WordPress site you've **linked** to a local copy, the Search tab can pull
|
|
665
|
+
the production database down — the same idea as a hand-rolled `wp db export` +
|
|
666
|
+
`scp`, without leaving the dashboard. Both actions are **read-only on production**
|
|
667
|
+
(they export; they never write to the live site) and run `ssh`/`scp`
|
|
668
|
+
non-interactively, so your key needs to be loaded in your agent.
|
|
669
|
+
|
|
670
|
+
- **Download a backup (`d`).** Exports the production database with `wp-cli` into
|
|
671
|
+
a stage file *outside* the public webroot, gzips it, downloads it into the linked
|
|
672
|
+
copy's `sql/` folder, and removes the remote copy. Needs **no local WP-CLI** —
|
|
673
|
+
the export runs on the server. Available whenever a WordPress site is linked. A
|
|
674
|
+
spinner on the site's row tracks an in-flight download even if you close the
|
|
675
|
+
overlay; the saved path and size are shown on completion.
|
|
676
|
+
- **Pull production → local (`p`, opt-in).** A full refresh of your **local**
|
|
677
|
+
database from production: it backs up the local DB first (to
|
|
678
|
+
`sql/local_<timestamp>.sql.gz`), exports + downloads production, imports it
|
|
679
|
+
locally, rewrites production URLs → your local URL (`wp search-replace`), and
|
|
680
|
+
runs an optional `bin/sync.d/post-import.sh` hook if the project has one.
|
|
681
|
+
**This overwrites your local database**, so it's **off by default** — enable it
|
|
682
|
+
with `localSync` (see "Optional settings"). It needs a working local WP-CLI; if
|
|
683
|
+
it's missing you get a clear error rather than a broken run.
|
|
684
|
+
|
|
685
|
+
Everything is detected automatically, for Standard WP **and** Bedrock:
|
|
686
|
+
|
|
687
|
+
- the **remote document root** from the API (`/sites/{domain}/files{public_folder}`),
|
|
688
|
+
- the **SSH target** from the site/server (`{site_user}@{server_ip}`),
|
|
689
|
+
- the **local WordPress root** from the linked path (where `wp` runs — wp-config
|
|
690
|
+
for Standard WP, `wp-cli.yml` for Bedrock),
|
|
691
|
+
- the **local URL** for the rewrite from the link's local URL, falling back to the
|
|
692
|
+
project's `.env` `WP_HOME`.
|
|
693
|
+
|
|
694
|
+
If your project already has a `bin/sync.d/post-import.sh` (e.g. Elementor URL
|
|
695
|
+
swaps, plugin toggles), it runs with `WEB_DIR`, `SYNC_REMOTE_HOST`, and
|
|
696
|
+
`SYNC_LOCAL_HOST` set — so existing per-project tweaks carry over with no extra
|
|
697
|
+
configuration. When a project has **no** hook yet, the `p` confirm screen
|
|
698
|
+
explains what the hook is and offers `s` to scaffold an **inert** sample
|
|
699
|
+
`bin/sync.d/post-import.sh` (every example commented out, documented with the env
|
|
700
|
+
vars above) for you to edit. Backups stay **gzipped** in `sql/`; decompress with
|
|
701
|
+
`gunzip` when you need one.
|
|
702
|
+
|
|
703
|
+
Both `d` and `p` show their progress as a **building checklist** inside the
|
|
704
|
+
overlay — each step gets a `✓` as it completes, the running step spins, and a
|
|
705
|
+
failure marks the exact step with `✕` — so you can see everything that happened,
|
|
706
|
+
ending with the saved paths. It keeps running if you close the overlay (`Esc`);
|
|
707
|
+
reopen with the same key to watch it through.
|
|
708
|
+
|
|
709
|
+
## Production media fallback
|
|
710
|
+
|
|
711
|
+
A `p` sync refreshes your local **database**, but not the media library — so the
|
|
712
|
+
local site shows broken images. Syncing the files is often impractical (some
|
|
713
|
+
libraries are hundreds of thousands of items). Instead, press **`m`** on a linked
|
|
714
|
+
WordPress site to serve any image that's **missing locally** straight from
|
|
715
|
+
production.
|
|
716
|
+
|
|
717
|
+
It works by dropping a small, self-contained mu-plugin into your local copy
|
|
718
|
+
(`wp-content/mu-plugins/` or Bedrock's `web/app/mu-plugins/`). The plugin rewrites
|
|
719
|
+
any uploads URL whose file isn't present locally to the production origin —
|
|
720
|
+
covering the standard media functions, page-builder output (e.g. Elementor inline
|
|
721
|
+
CSS and gallery data), and **legacy paths** left over from a Standard-WP →
|
|
722
|
+
Bedrock conversion. Because it decides "missing" from the real document root and
|
|
723
|
+
redirects to the **same path** on production, your production routing (CDN/S3
|
|
724
|
+
redirects included) resolves whatever it's handed.
|
|
725
|
+
|
|
726
|
+
- **It runs in WordPress, not your web server**, so it works the same on Valet,
|
|
727
|
+
Herd, LocalWP, DDEV, MAMP — anything. No nginx/Apache config to get right.
|
|
728
|
+
- **Local-only and read-only on production** — it just hotlinks your own
|
|
729
|
+
production images. It self-disables when running on the production domain, so
|
|
730
|
+
it's inert if it ever gets deployed.
|
|
731
|
+
- **The plugin's presence is the on/off state** (no config flag). Press `m` to
|
|
732
|
+
toggle; `u` from the overlay updates it in place when a newer version ships.
|
|
733
|
+
- Needs production reachable and hotlinking allowed (it won't work behind staging
|
|
734
|
+
Basic-Auth). Images delivered by external CSS files or async JS/REST are the one
|
|
735
|
+
thing server-side rewriting can't catch.
|
|
736
|
+
|
|
737
|
+
## DNS hosts, access & editing
|
|
738
|
+
|
|
739
|
+
A **server-migration lens** for DNS: see the records that move a site to another
|
|
740
|
+
server, and edit them in place. It is deliberately **not** a full zone editor — it
|
|
741
|
+
only ever shows and touches a site's own hosting records (its apex / `www` /
|
|
742
|
+
subdomains and additional domains), so your MX, TXT, DKIM, and other zone records are
|
|
743
|
+
never shown or changed. Moving a site can't take down its email.
|
|
744
|
+
|
|
745
|
+
- **The view.** Press `n` on a site for just that site's records, or `N` on a server
|
|
746
|
+
for every site on it; inside a site-scoped view, `a` expands to the whole server.
|
|
747
|
+
Each **site** is a line, labeled by its own domain (even when it's a subdomain),
|
|
748
|
+
with its hosting record's type, **TTL in seconds**, value, a `◀ here` flag when the
|
|
749
|
+
record points at this server, and a `+www` tag when `www` simply follows the apex.
|
|
750
|
+
A site's additional domains nest beneath it, so a domain portfolio reads as one
|
|
751
|
+
site, not many. TTLs come from the zone's authoritative nameserver (the configured
|
|
752
|
+
value, not a counted-down one), so they show even for hosts you haven't connected;
|
|
753
|
+
`r` refreshes.
|
|
754
|
+
- **Access (`✓ ↗ ○ ·`).** Each record's zone shows whether you can edit it: `✓`
|
|
755
|
+
editable, `↗` web-only handoff, `○` the provider has an API but you haven't
|
|
756
|
+
connected an account that holds the zone, `·` unknown. A zone is `✓` only when a
|
|
757
|
+
connected account of the provider that actually serves it (its live nameservers)
|
|
758
|
+
holds it — so a stale or duplicate zone elsewhere never shows a false green. With
|
|
759
|
+
two or more accounts connected, an **ACCOUNT** column names the owning one.
|
|
760
|
+
- **Edit a TTL (`⏎`).** On an editable record, `⏎` opens a focused editor — pick a
|
|
761
|
+
preset or a custom value, confirm, and it's written to **AWS Route 53** or
|
|
762
|
+
**Cloudflare** through your connected account. This is the prep step for a low-risk
|
|
763
|
+
migration: lower the TTL, cut over, then restore it. Before writing, an edit-time
|
|
764
|
+
check re-reads the live nameservers and **blocks** the change if the connected
|
|
765
|
+
account's zone isn't the one actually serving the domain. Route 53 changes are
|
|
766
|
+
followed to completion; the record shows an "updating" status that keeps ticking
|
|
767
|
+
even if you leave the view. Cloudflare **proxied** records keep their TTL fixed
|
|
768
|
+
to automatic, so it's not editable here — repointing still works (next).
|
|
769
|
+
- **Repoint a record (`p`).** The same focused editor, on the record's **value** —
|
|
770
|
+
where it points. The picker leads with **your SpinupWP servers** (name + IP, the
|
|
771
|
+
record's current home tagged), because pointing a record at one of your own boxes
|
|
772
|
+
is what this is for; a custom IP is the fallback. The same confirm and NS
|
|
773
|
+
pre-flight gate apply, the write is followed to completion in the background, and
|
|
774
|
+
the inventory row shows `→ new IP` while it applies. A Cloudflare **proxied**
|
|
775
|
+
record repoints its **origin** behind the proxy (visitors keep resolving
|
|
776
|
+
Cloudflare's IPs). CNAMEs aren't repointed — they follow their target, which is
|
|
777
|
+
the record to edit. This is the standalone version of the clone wizard's DNS
|
|
778
|
+
cutover: migrate a single site, finish a cutover by hand, or fix a stale record.
|
|
779
|
+
- **Connect a provider (`c`).** Manage credentials for the selected zone's
|
|
780
|
+
provider — **AWS Route 53** (an IAM access key), **Cloudflare** (a scoped token),
|
|
781
|
+
or **GoDaddy** (a Production API key). Multiple accounts per provider are
|
|
782
|
+
supported, with a drill-down into each account's zones. Credentials are verified
|
|
783
|
+
before they're stored, kept in `config.json` (chmod 600), and the matching
|
|
784
|
+
environment variables are honored (`CLOUDFLARE_API_TOKEN`, `AWS_ACCESS_KEY_ID` /
|
|
785
|
+
`AWS_SECRET_ACCESS_KEY`, `GODADDY_API_KEY` / `GODADDY_API_SECRET`). Secrets are
|
|
786
|
+
masked as you type. Listing hosts needs only read access (Cloudflare `Zone:Read`);
|
|
787
|
+
**editing (a TTL or a repoint)** needs write access — Route 53 record writes, or a
|
|
788
|
+
Cloudflare `Zone.DNS:Edit` token.
|
|
789
|
+
- **Web-only hosts (GoDaddy, Namecheap, Network Solutions, …).** A registrar with
|
|
790
|
+
no usable API shows `↗`; press `w` (in the inventory or the connect overlay) to
|
|
791
|
+
open its web console — for GoDaddy specifically, your Clients hub, with the
|
|
792
|
+
domain copied to your clipboard so you can paste it after logging in as the
|
|
793
|
+
client. Every `↗` zone also shows an **access note** — "Delegate Access" by
|
|
794
|
+
default (the assumed-normal case for a client's registrar), or your own
|
|
795
|
+
per-zone override (e.g. an IT vendor's contact) when that default doesn't hold.
|
|
796
|
+
Press `c` on any such zone to open **Manage Access**: it lists every zone
|
|
797
|
+
already known at that host, `n` edits the selected zone's note, and `r`
|
|
798
|
+
resolves your whole fleet's DNS to fill in any zones it hasn't seen yet.
|
|
799
|
+
|
|
800
|
+
Provider credentials are optional — without them you still get the full host
|
|
801
|
+
inventory and TTLs, just without the editable/account columns or in-place editing.
|
|
802
|
+
|
|
803
|
+
## Development
|
|
804
|
+
|
|
805
|
+
```sh
|
|
806
|
+
bun run dev # run from source
|
|
807
|
+
bun run typecheck # tsc --noEmit
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### Dev Mode (demos with fake data)
|
|
811
|
+
|
|
812
|
+
```sh
|
|
813
|
+
SPINUP_DEV_MODE=1 bun run dev
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Boots straight into the dashboard against a small in-memory example fleet
|
|
817
|
+
(`web1.example.com` … `web9.example.org`, a mix of Standard WP, Bedrock, and
|
|
818
|
+
non-WP sites) — no API token, no network calls, nothing that can touch a real
|
|
819
|
+
account. A purple **`DEV MODE`** badge in the header makes it unmistakable. Every
|
|
820
|
+
write action (PHP upgrade, HTTPS toggle, purge cache, reboot, create a server/site)
|
|
821
|
+
works against the fake data and shows the same in-progress/toast behavior as the
|
|
822
|
+
real thing, so it's useful for screenshots, walkthroughs, and UI work without a
|
|
823
|
+
live account on hand. The clone wizard's SSH-based file/DB pull is out of scope —
|
|
824
|
+
it always talks to real servers over SSH, independent of the API client.
|
|
825
|
+
|
|
826
|
+
The fixture fleet lives in `src/dev/fixtures.ts`; the fake client it's served
|
|
827
|
+
through is `src/dev/mockClient.ts`.
|
|
828
|
+
|
|
829
|
+
### Project layout
|
|
830
|
+
|
|
831
|
+
```
|
|
832
|
+
src/
|
|
833
|
+
index.tsx entry — boots OpenTUI, routes onboarding vs app
|
|
834
|
+
config.ts token resolution + persistence
|
|
835
|
+
api/
|
|
836
|
+
client.ts typed fetch client (reads + writes, errors, validation)
|
|
837
|
+
types.ts Server / Site / Event types
|
|
838
|
+
dev/ Dev Mode: fake fleet + client (SPINUP_DEV_MODE, see above)
|
|
839
|
+
lib/ formatting, theme, open-in-browser, SSH helpers
|
|
840
|
+
stack.ts Tier-1 stack classification + effective (probe-aware) bucket
|
|
841
|
+
probe.ts Tier-2 SSH stack probe (WHMCS / Bedrock / Laravel / WP / …)
|
|
842
|
+
stackCache.ts disk-backed probe cache (hydrate on start, write-through)
|
|
843
|
+
phpEol.ts PHP EOL dates + the version set offered by the upgrade picker
|
|
844
|
+
ubuntuEol.ts Ubuntu LTS EOL dates (same embedded+refresh pattern as phpEol.ts)
|
|
845
|
+
ui/
|
|
846
|
+
App.tsx shell: splash gating, key routing, layout
|
|
847
|
+
store.tsx React-context data store
|
|
848
|
+
Splash / Onboarding / Header / StatusBar / Help
|
|
849
|
+
List.tsx generic windowed keyboard list
|
|
850
|
+
Details.tsx shared server/site detail panels
|
|
851
|
+
views/ Dashboard, Browser, Stacks, Search, Events, Health, PhpUpgrade
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## License
|
|
855
|
+
|
|
856
|
+
MIT — see [LICENSE](LICENSE).
|