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.
- package/CHANGELOG.md +70 -1
- package/README.md +124 -578
- package/package.json +1 -1
- package/src/dev/fixtures.ts +494 -5
- 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/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 }} />
|