spinuptui 0.19.1 → 0.20.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 +52 -1
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/lib/serverClone.ts +26 -4
- package/src/lib/stack.ts +23 -0
- package/src/lib/wpCli.ts +30 -0
- package/src/lib/wpInventory.ts +7 -3
- package/src/ui/store.tsx +8 -11
- package/src/ui/views/CloneWizard.tsx +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,56 @@ versions; such changes are called out here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.20.0] - 2026-07-08
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **The clone wizard's existing-server picker now shows each server's current
|
|
18
|
+
site count.** Picking a destination previously showed only provider/region/
|
|
19
|
+
IP — no sense of how busy a candidate already is. Each row now leads with
|
|
20
|
+
`N site(s)`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **The clone wizard no longer routes git-deployed non-WordPress sites through
|
|
24
|
+
a Bedrock pull.** `cloneStackFor` trusted `git.repo` alone as "this is
|
|
25
|
+
Bedrock" — true often enough (`is_wordpress` is unreliable for git sites),
|
|
26
|
+
but a git-deployed **static** site (confirmed live: a client's static site,
|
|
27
|
+
repo literally named `*-static-site`) would be routed through
|
|
28
|
+
`composer install`/wp-cli anyway, which it can't survive. A conclusive
|
|
29
|
+
non-WordPress probe (`whmcs`/`laravel`/`static`) now overrides `git.repo`;
|
|
30
|
+
a probe saying `bedrock`/`wordpress`, or no probe at all, still trusts it
|
|
31
|
+
(same "probe the site (`d`) before cloning" caveat as the sibling fix
|
|
32
|
+
above).
|
|
33
|
+
- **The clone wizard no longer silently skips the database for API-mislabeled
|
|
34
|
+
WordPress sites.** Its stack picker read `is_wordpress` straight from the
|
|
35
|
+
API, unlike every other view (Stacks, Browser), which corrects that flag
|
|
36
|
+
against a `d` probe result. A site the API wrongly reports as non-WordPress
|
|
37
|
+
(confirmed live against two real client sites) would default to a
|
|
38
|
+
files-only pull — filesystem copied, database never touched, so the
|
|
39
|
+
"clone" wasn't a working WordPress site. The wizard now consults the same
|
|
40
|
+
probe-corrected classification: **probe the site (`d`) before cloning** so
|
|
41
|
+
the wizard picks it up as WordPress.
|
|
42
|
+
- **wp-cli reads now use the site's own configured PHP version, not the
|
|
43
|
+
server's system default.** wp-cli's installed binary shebangs
|
|
44
|
+
`#!/usr/bin/env php`, which resolves through `PATH` to whichever PHP a
|
|
45
|
+
server's `update-alternatives` currently calls default — independent of
|
|
46
|
+
any individual site's configured version, and it can be missing extensions
|
|
47
|
+
that version's CLI never got configured with. Confirmed live: a site
|
|
48
|
+
configured for PHP 8.1 silently ran its wp-cli reads under the server's
|
|
49
|
+
default 8.4, whose CLI was missing `mysqli` — every WordPress-content read
|
|
50
|
+
(posts, pages, users, active plugins, site/home URL) failed silently,
|
|
51
|
+
while file-only or native-MySQL-client commands (`core version`, `db
|
|
52
|
+
export`/`import`/`check`) were unaffected. This broke the clone wizard's
|
|
53
|
+
**Verify** step (`v`) and the **installed plugins & themes** view (`p`) for
|
|
54
|
+
any site whose configured PHP differs from the server's system default —
|
|
55
|
+
both now pin wp-cli to the site's own PHP-CLI binary, falling back to the
|
|
56
|
+
system default only if that specific version isn't installed.
|
|
57
|
+
- **Verify now tells the difference between "the clone actually differs" and
|
|
58
|
+
"wp-cli couldn't read one side's database."** If `core version` succeeds
|
|
59
|
+
(a file read, no DB) but every database-backed fact comes back empty, that's
|
|
60
|
+
the missing-extension failure above, not a real content difference — Verify
|
|
61
|
+
now reports it plainly instead of a wall of misleading ✕ rows implying data
|
|
62
|
+
loss.
|
|
63
|
+
|
|
14
64
|
## [0.19.1] - 2026-07-08
|
|
15
65
|
|
|
16
66
|
### Fixed
|
|
@@ -929,7 +979,8 @@ Initial tagged release.
|
|
|
929
979
|
### Notes
|
|
930
980
|
- Read-only release: works with a SpinupWP **Read Only** API token.
|
|
931
981
|
|
|
932
|
-
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.
|
|
982
|
+
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.20.0...HEAD
|
|
983
|
+
[0.20.0]: https://github.com/mwender/spinupwp-tui/compare/v0.19.1...v0.20.0
|
|
933
984
|
[0.19.1]: https://github.com/mwender/spinupwp-tui/compare/v0.19.0...v0.19.1
|
|
934
985
|
[0.19.0]: https://github.com/mwender/spinupwp-tui/compare/v0.18.0...v0.19.0
|
|
935
986
|
[0.18.0]: https://github.com/mwender/spinupwp-tui/compare/v0.17.0...v0.18.0
|
package/README.md
CHANGED
|
@@ -592,7 +592,8 @@ over SSH). The steps:
|
|
|
592
592
|
`space` toggles). SpinupTUI sizes each one live (disk + database) into a payload total
|
|
593
593
|
so you know what you're moving; a concurrency cap protects the busy source.
|
|
594
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
|
|
595
|
+
`c` flow), or `d` to pick an existing server as the target — each listed with its
|
|
596
|
+
current site count, so you can see at a glance how busy a candidate already is.
|
|
596
597
|
3. **Connect** — connect sudo on **both** servers. The clone is a server-to-server
|
|
597
598
|
**pull**: the destination pulls each site directly from the source over SSH (no
|
|
598
599
|
bytes routed through your laptop), authenticating with a key granted onto the source
|
package/package.json
CHANGED
package/src/lib/serverClone.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// that shape the Bedrock branch).
|
|
12
12
|
|
|
13
13
|
import type { Server } from "../api/types.ts"
|
|
14
|
+
import { wpCliResolveScript } from "./wpCli.ts"
|
|
14
15
|
|
|
15
16
|
const SSH_OPTS = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new"]
|
|
16
17
|
|
|
@@ -700,6 +701,7 @@ export interface VerifySpec {
|
|
|
700
701
|
publicFolder?: string // configured public folder — fallback when the pull's rels are unknown
|
|
701
702
|
sourceWebrootRel?: string // DETECTED source layout from the pull ("" = files root)
|
|
702
703
|
destWebrootRel?: string // the CLONE's WP dir from the pull (post-normalization)
|
|
704
|
+
phpVersion?: string | null // the site's configured PHP version — see wpCli.ts
|
|
703
705
|
}
|
|
704
706
|
|
|
705
707
|
// One round-trip per side: collect labeled wp-cli facts as `key=value` lines.
|
|
@@ -707,10 +709,14 @@ export interface VerifySpec {
|
|
|
707
709
|
// stalled outbound call once ate the whole budget and cut the facts off mid-script) —
|
|
708
710
|
// and the flags are identical on both sides, so every count stays comparable. The
|
|
709
711
|
// trailing `eof` sentinel is how verifyClone tells "read got cut off" from "differs".
|
|
710
|
-
|
|
712
|
+
// wp-cli is pinned to the site's OWN configured PHP-CLI (wpCliResolveScript) rather
|
|
713
|
+
// than bare `wp` — see wpCli.ts for why (a server's system-default PHP can silently
|
|
714
|
+
// differ from, and be missing extensions present in, the site's actual version).
|
|
715
|
+
async function wpFacts(ctx: SudoCtx, root: string, user: string, phpVersion?: string | null): Promise<Record<string, string>> {
|
|
711
716
|
const script = [
|
|
712
717
|
`cd ${shq(root)} 2>/dev/null || exit 0`,
|
|
713
|
-
|
|
718
|
+
wpCliResolveScript(phpVersion),
|
|
719
|
+
`u() { sudo -u ${shq(user)} -H "$PHP" "$WP" --skip-plugins --skip-themes "$@" 2>/dev/null; }`,
|
|
714
720
|
`echo "core=$(u core version)"`,
|
|
715
721
|
`echo "posts=$(u post list --post_type=any --format=count)"`,
|
|
716
722
|
`echo "pages=$(u post list --post_type=page --format=count)"`,
|
|
@@ -753,8 +759,8 @@ export async function verifyClone(source: SudoCtx, dest: SudoCtx, spec: VerifySp
|
|
|
753
759
|
const srcDir = dirFor(spec.sourceWebrootRel)
|
|
754
760
|
const destDir = dirFor(spec.destWebrootRel)
|
|
755
761
|
const [sf, cf, http] = await Promise.all([
|
|
756
|
-
wpFacts(source, srcDir, spec.sourceSiteUser),
|
|
757
|
-
wpFacts(dest, destDir, spec.destSiteUser),
|
|
762
|
+
wpFacts(source, srcDir, spec.sourceSiteUser, spec.phpVersion),
|
|
763
|
+
wpFacts(dest, destDir, spec.destSiteUser, spec.phpVersion),
|
|
758
764
|
curlStatus(spec.domain, spec.destIp),
|
|
759
765
|
])
|
|
760
766
|
// A missing tail sentinel means the facts script was cut off partway (timeout, a
|
|
@@ -764,6 +770,22 @@ export async function verifyClone(source: SudoCtx, dest: SudoCtx, spec: VerifySp
|
|
|
764
770
|
const side = sf.eof !== "1" ? "source" : "clone"
|
|
765
771
|
throw new Error(`couldn't read the ${side}'s WordPress facts (the wp-cli read was cut off) — press v to re-run`)
|
|
766
772
|
}
|
|
773
|
+
// `core` succeeding (a file read, no DB) while EVERY database-backed fact comes
|
|
774
|
+
// back empty is a distinct signature from a real content difference (which shows
|
|
775
|
+
// differing NON-empty values, not blanks) — it means wp-cli's PHP-CLI couldn't
|
|
776
|
+
// reach the database on that side, commonly a missing `mysqli` extension for
|
|
777
|
+
// whatever PHP version it resolved to (see wpCli.ts). Surface that plainly
|
|
778
|
+
// instead of a wall of misleading ✕ rows implying the clone lost data.
|
|
779
|
+
const dbFields = ["posts", "pages", "users", "plugins", "siteurl", "home"] as const
|
|
780
|
+
const looksBroken = (f: Record<string, string>) => !!f.core && dbFields.every((k) => !f[k])
|
|
781
|
+
const srcBroken = looksBroken(sf)
|
|
782
|
+
const cloneBroken = looksBroken(cf)
|
|
783
|
+
if (srcBroken || cloneBroken) {
|
|
784
|
+
const side = srcBroken && cloneBroken ? "both the source and the clone" : srcBroken ? "the source" : "the clone"
|
|
785
|
+
throw new Error(
|
|
786
|
+
`wp-cli can't reach ${side}'s database over PHP (commonly a missing "mysqli" extension for whatever PHP-CLI it resolved to) — this looks like a server/PHP-CLI environment issue, not an actual data difference.`,
|
|
787
|
+
)
|
|
788
|
+
}
|
|
767
789
|
const checks: VerifyCheck[] = []
|
|
768
790
|
checks.push({ key: "http", label: "Clone serves (HTTP)", source: "—", clone: http, ok: /^[23]\d\d$/.test(http) })
|
|
769
791
|
const cmp = (key: string, label: string) => {
|
package/src/lib/stack.ts
CHANGED
|
@@ -49,6 +49,29 @@ export function effectiveStack(site: Site, probeKind?: ProbeKind | null): Stack
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Which pull the clone wizard runs for a site — the SAME effectiveStack a
|
|
53
|
+
// probe corrects elsewhere (Stacks tab, Browser), so an API-mislabeled site
|
|
54
|
+
// (is_wordpress=false but really WordPress — confirmed live against real
|
|
55
|
+
// client sites, 2026-07-08) gets a real database pull once probed, instead
|
|
56
|
+
// of silently defaulting to a files-only copy. git.repo still takes
|
|
57
|
+
// precedence for Bedrock detection (the pull needs the repo URL, which a
|
|
58
|
+
// probe alone can't supply) — is_wordpress is unreliable for git sites, so
|
|
59
|
+
// absent a probe (or a WP-family one) git.repo remains the best available
|
|
60
|
+
// signal. But git.repo alone only proves "deployed via git," not "is
|
|
61
|
+
// Bedrock" — a git-deployed STATIC site (confirmed live, 2026-07-09: a
|
|
62
|
+
// client's git-deployed static site, repo literally named
|
|
63
|
+
// "*-static-site") would otherwise be routed through a Bedrock pull
|
|
64
|
+
// (composer install, wp-cli) it can't survive. A CONCLUSIVE non-WordPress
|
|
65
|
+
// probe (whmcs/laravel/static) overrides git.repo; a probe saying
|
|
66
|
+
// "bedrock"/"wordpress", or no probe at all, still trusts git.repo.
|
|
67
|
+
export function cloneStackFor(site: Site, probeKind?: ProbeKind | null): "wp" | "bedrock" | "files" {
|
|
68
|
+
if (site.git?.repo) {
|
|
69
|
+
if (probeKind === "whmcs" || probeKind === "laravel" || probeKind === "static") return "files"
|
|
70
|
+
return "bedrock"
|
|
71
|
+
}
|
|
72
|
+
return effectiveStack(site, probeKind) === "Standard WP" ? "wp" : "files"
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
// `onSelection` brightens the colors that read poorly on the focused
|
|
53
76
|
// (bright-green) selection background (green-on-green Bedrock, the faint grey).
|
|
54
77
|
export function stackColor(stack: Stack, onSelection = false): string {
|
package/src/lib/wpCli.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Resolve wp-cli through the SITE's own configured PHP-CLI, not the bare `wp`
|
|
2
|
+
// command. wp-cli's installed binary shebangs `#!/usr/bin/env php`, which
|
|
3
|
+
// resolves through PATH to the SERVER's system-DEFAULT PHP — which drifts
|
|
4
|
+
// toward whatever version was installed most recently, independent of any
|
|
5
|
+
// individual site's configured version, and can be missing extensions that
|
|
6
|
+
// version's CLI SAPI never got configured with.
|
|
7
|
+
//
|
|
8
|
+
// Verified live 2026-07-09 on web4.wenmarkdigital.com: the system default had
|
|
9
|
+
// drifted to 8.4, whose CLI lacked `mysqli` (its FPM pool likely never needed
|
|
10
|
+
// it — no site there runs 8.4), while lp.anchoredconstructiontn.com's actual
|
|
11
|
+
// configured version (8.1) has a fully working php8.1-cli. Bare `wp` silently
|
|
12
|
+
// ran under 8.4 and failed on anything touching $wpdb (post/user/plugin list,
|
|
13
|
+
// option get) — while `wp core version`, and `wp db export/import/check/size`
|
|
14
|
+
// (they shell out to native mysql client tools, not through mysqli) were
|
|
15
|
+
// unaffected. Confirmed the fix directly: `/usr/bin/php8.1 /usr/local/bin/wp
|
|
16
|
+
// post list ...` returned correct data where bare `wp` errored.
|
|
17
|
+
//
|
|
18
|
+
// Only applies to REMOTE (SSH) wp-cli calls against SpinupWP-managed servers,
|
|
19
|
+
// which have this specific multi-PHP-version-drift shape. Local dev-machine
|
|
20
|
+
// wp-cli (e.g. dbSync.ts's prod→local pull) is a different environment with
|
|
21
|
+
// its own PHP entirely and is not affected — don't apply this there.
|
|
22
|
+
|
|
23
|
+
// Emits POSIX shell that resolves $WP (the wp-cli binary) and $PHP (the
|
|
24
|
+
// version-pinned interpreter, falling back to the bare `php` on PATH if the
|
|
25
|
+
// versioned binary isn't installed — never worse than today's behavior).
|
|
26
|
+
// Callers invoke `"$PHP" "$WP" ...` in place of bare `wp ...`.
|
|
27
|
+
export function wpCliResolveScript(phpVersion?: string | null): string {
|
|
28
|
+
const phpBin = phpVersion ? `php${phpVersion}` : "php"
|
|
29
|
+
return [`WP=$(command -v wp 2>/dev/null || echo /usr/local/bin/wp)`, `PHP=$(command -v ${phpBin} 2>/dev/null || command -v php)`].join("\n")
|
|
30
|
+
}
|
package/src/lib/wpInventory.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { Server, Site } from "../api/types.ts"
|
|
12
12
|
import { SSH_OPTS, sshPort } from "./dbBackup.ts"
|
|
13
13
|
import { detectWpDirScript } from "./serverClone.ts"
|
|
14
|
+
import { wpCliResolveScript } from "./wpCli.ts"
|
|
14
15
|
|
|
15
16
|
// One plugin or theme, mirroring `wp {plugin,theme} list` columns.
|
|
16
17
|
export interface WpItem {
|
|
@@ -100,13 +101,16 @@ export async function fetchWpInventory(
|
|
|
100
101
|
const remote = [
|
|
101
102
|
detectWpDirScript(root, site.public_folder ?? undefined),
|
|
102
103
|
`if [ -z "$W" ]; then echo ===NOTWP; echo ===END; exit 0; fi`,
|
|
103
|
-
|
|
104
|
+
// Pinned to the site's OWN configured PHP-CLI — bare `wp` resolves through
|
|
105
|
+
// its shebang to the server's system-default PHP, which can differ from
|
|
106
|
+
// (and lack extensions present in) the site's actual version. See wpCli.ts.
|
|
107
|
+
wpCliResolveScript(site.php_version),
|
|
104
108
|
`echo ===WPDIR; printf '%s\\n' "$W"`,
|
|
105
109
|
// The trailing `echo` after each wp command is load-bearing: `wp --format=json`
|
|
106
110
|
// emits NO trailing newline, so without it the next ===MARKER glues onto the end
|
|
107
111
|
// of the JSON line — the marker never parses and JSON.parse chokes on it.
|
|
108
|
-
`echo ===PLUGINS; "$WP" --path="$W" plugin list --fields=${fields} --format=json 2>/dev/null || true; echo`,
|
|
109
|
-
`echo ===THEMES; "$WP" --path="$W" theme list --fields=${fields} --format=json 2>/dev/null || true; echo`,
|
|
112
|
+
`echo ===PLUGINS; "$PHP" "$WP" --path="$W" plugin list --fields=${fields} --format=json 2>/dev/null || true; echo`,
|
|
113
|
+
`echo ===THEMES; "$PHP" "$WP" --path="$W" theme list --fields=${fields} --format=json 2>/dev/null || true; echo`,
|
|
110
114
|
`echo ===END`,
|
|
111
115
|
].join("\n")
|
|
112
116
|
|
package/src/ui/store.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { APP_VERSION } from "../version.ts"
|
|
|
17
17
|
import { cachedUpdateInfo, refreshUpdateInfo, type UpdateInfo } from "../lib/appUpdate.ts"
|
|
18
18
|
import { fetchReleaseNotes, type ReleaseNotesInfo } from "../lib/releaseNotes.ts"
|
|
19
19
|
import { resolveLocalLink, expandPath, normalizeLink, type LocalLink } from "../lib/local.ts"
|
|
20
|
-
import type
|
|
20
|
+
import { cloneStackFor, type Stack } from "../lib/stack.ts"
|
|
21
21
|
import { openTerminalAt, openUrl, openSshSession } from "../lib/open.ts"
|
|
22
22
|
import { gitDrift, type Drift } from "../lib/gitStatus.ts"
|
|
23
23
|
import { probeSite } from "../lib/probe.ts"
|
|
@@ -344,12 +344,6 @@ export function cloneNeedsGitAccess(j: CloneJob): boolean {
|
|
|
344
344
|
export function isCloneInFlight(j: CloneJob | null | undefined): boolean {
|
|
345
345
|
return j != null && j.step !== "done" && j.step !== "error"
|
|
346
346
|
}
|
|
347
|
-
// Detect a site's stack the way the clone branches: a git repo → Bedrock
|
|
348
|
-
// (is_wordpress is unreliable for git sites — see the API findings doc);
|
|
349
|
-
// is_wordpress → Standard WP; anything else → files-only (no DB, no wp-cli).
|
|
350
|
-
export function cloneStackFor(site: Site): "wp" | "bedrock" | "files" {
|
|
351
|
-
return site.git?.repo ? "bedrock" : site.is_wordpress ? "wp" : "files"
|
|
352
|
-
}
|
|
353
347
|
// Alphanumeric token for generated dest DB passwords (never printed/persisted).
|
|
354
348
|
function randomToken(n: number): string {
|
|
355
349
|
const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
@@ -3335,8 +3329,11 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
3335
3329
|
}
|
|
3336
3330
|
const srcSites = sitesForServer(server.id)
|
|
3337
3331
|
const sites: CloneSiteState[] = srcSites.map((s) => {
|
|
3338
|
-
|
|
3339
|
-
|
|
3332
|
+
// Probe-corrected: an API-mislabeled site (is_wordpress=false but really
|
|
3333
|
+
// WordPress) gets a real database pull once probed (`d`), instead of
|
|
3334
|
+
// silently defaulting to a files-only copy — see cloneStackFor's comment.
|
|
3335
|
+
const stack = cloneStackFor(s, probes.get(s.id)?.result.kind)
|
|
3336
|
+
const isWordPress = stack !== "files"
|
|
3340
3337
|
return {
|
|
3341
3338
|
sourceSiteId: s.id,
|
|
3342
3339
|
domain: s.domain,
|
|
@@ -3371,7 +3368,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
3371
3368
|
setCloneServer(server)
|
|
3372
3369
|
setCloneJob(draft)
|
|
3373
3370
|
},
|
|
3374
|
-
[cloneJob, sitesForServer, servers],
|
|
3371
|
+
[cloneJob, sitesForServer, servers, probes],
|
|
3375
3372
|
)
|
|
3376
3373
|
|
|
3377
3374
|
// Background an in-flight clone: hide the wizard but KEEP the job running (the
|
|
@@ -3770,7 +3767,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
3770
3767
|
try {
|
|
3771
3768
|
const res = site.stack === "files"
|
|
3772
3769
|
? await verifyFilesClone(source, dest, { domain: site.domain, destIp })
|
|
3773
|
-
: await verifyClone(source, dest, { domain: site.domain, sourceSiteUser: site.siteUser, destSiteUser: site.siteUser, destIp, publicFolder: site.stack === "wp" ? site.publicFolder : undefined, sourceWebrootRel: site.stack === "wp" ? site.sourceWebrootRel : undefined, destWebrootRel: site.stack === "wp" ? site.destWebrootRel : undefined })
|
|
3770
|
+
: await verifyClone(source, dest, { domain: site.domain, sourceSiteUser: site.siteUser, destSiteUser: site.siteUser, destIp, publicFolder: site.stack === "wp" ? site.publicFolder : undefined, sourceWebrootRel: site.stack === "wp" ? site.sourceWebrootRel : undefined, destWebrootRel: site.stack === "wp" ? site.destWebrootRel : undefined, phpVersion: site.phpVersion })
|
|
3774
3771
|
setCloneSite(siteId, (s) => ({ ...s, verifying: false, verify: res }))
|
|
3775
3772
|
} catch (err) {
|
|
3776
3773
|
setCloneSite(siteId, (s) => ({ ...s, verifying: false, verifyError: (err as Error).message }))
|
|
@@ -48,6 +48,7 @@ export function CloneWizard() {
|
|
|
48
48
|
cloneServer: server,
|
|
49
49
|
cloneJob: job,
|
|
50
50
|
servers,
|
|
51
|
+
sitesForServer,
|
|
51
52
|
accountSlug,
|
|
52
53
|
toggleCloneSite,
|
|
53
54
|
toggleCloneSiteUploads,
|
|
@@ -473,7 +474,8 @@ export function CloneWizard() {
|
|
|
473
474
|
<box style={{ height: 1 }} />
|
|
474
475
|
{eligibleDests.map((s, i) => {
|
|
475
476
|
const sel = i === destIdx
|
|
476
|
-
const
|
|
477
|
+
const n = sitesForServer(s.id).length
|
|
478
|
+
const meta = `${n} site${n === 1 ? "" : "s"} · ${s.provider_name || "—"} · ${s.region || "—"}${s.ip_address ? " " + s.ip_address : ""}`
|
|
477
479
|
return (
|
|
478
480
|
<box key={s.id} style={{ flexDirection: "row", height: 1, backgroundColor: sel ? theme.bgAlt : undefined }}>
|
|
479
481
|
<text content={sel ? "❯ " : " "} fg={theme.brand} style={{ flexShrink: 0 }} />
|