spinuptui 0.19.1 → 0.20.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.
@@ -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
- `WP=$(command -v wp 2>/dev/null || echo /usr/local/bin/wp)`,
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 { Stack } from "../lib/stack.ts"
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
- const stack = cloneStackFor(s)
3339
- const isWordPress = s.is_wordpress
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 meta = `${s.provider_name || "—"} · ${s.region || "—"}${s.ip_address ? " " + s.ip_address : ""}`
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 }} />