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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Local working-copy link overlay — Phase 1 (link + view, no mutation).
|
|
2
|
+
//
|
|
3
|
+
// Opened with `L` on a selected site (Browser / Search results). Two modes:
|
|
4
|
+
// • view — the site is linked: shows the validated status (Bedrock / WordPress
|
|
5
|
+
// / missing) and offers open-locally actions: `t` opens a terminal at the
|
|
6
|
+
// path, `v` opens the stored local URL in the browser. `e` edits, `x` unlinks.
|
|
7
|
+
// • edit — a small form to enter the local path + local URL (manual entry).
|
|
8
|
+
//
|
|
9
|
+
// Per the locked spec these are LOCAL conveniences only: we open a terminal and
|
|
10
|
+
// the local URL — we never launch an editor and never run composer/git here.
|
|
11
|
+
// The mutating maintenance loop (composer update → push → deploy) is a later phase.
|
|
12
|
+
|
|
13
|
+
import { useEffect, useState } from "react"
|
|
14
|
+
import { useKeyboard } from "@opentui/react"
|
|
15
|
+
import { theme } from "../../lib/theme.ts"
|
|
16
|
+
import { truncate } from "../../lib/format.ts"
|
|
17
|
+
import { Panel, Centered } from "../components.tsx"
|
|
18
|
+
import { StatusBar } from "../StatusBar.tsx"
|
|
19
|
+
import { useStore } from "../store.tsx"
|
|
20
|
+
import { resolveLocalLink, type LocalKind } from "../../lib/local.ts"
|
|
21
|
+
|
|
22
|
+
type Mode = "view" | "edit"
|
|
23
|
+
type EditField = "path" | "url"
|
|
24
|
+
|
|
25
|
+
function kindColor(kind: LocalKind, exists: boolean): string {
|
|
26
|
+
if (!exists) return theme.bad
|
|
27
|
+
if (kind === "bedrock") return theme.good
|
|
28
|
+
if (kind === "wp") return theme.accent
|
|
29
|
+
return theme.warn
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function LocalLinkOverlay() {
|
|
33
|
+
const store = useStore()
|
|
34
|
+
const { localLinkSite: site, setLocalLinkSite, localLinks, linkSite, unlinkSite, setInputMode, openLocalTerminal, openLocalUrl, linkReturnToForgotten, setLinkReturnToForgotten, setForgottenOpen } = store
|
|
35
|
+
|
|
36
|
+
const existing = site ? localLinks.get(site.id) : undefined
|
|
37
|
+
|
|
38
|
+
const [mode, setMode] = useState<Mode>(() => (existing ? "view" : "edit"))
|
|
39
|
+
const [field, setField] = useState<EditField>("path")
|
|
40
|
+
const [pathInput, setPathInput] = useState(existing?.path ?? "")
|
|
41
|
+
const [urlInput, setUrlInput] = useState(existing?.localUrl ?? "")
|
|
42
|
+
const [flash, setFlash] = useState<string | null>(null)
|
|
43
|
+
|
|
44
|
+
// The form owns the keyboard while editing (suppresses global shortcuts so
|
|
45
|
+
// typing a path doesn't trigger navigation). Always release on unmount.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setInputMode(mode === "edit")
|
|
48
|
+
return () => setInputMode(false)
|
|
49
|
+
}, [mode, setInputMode])
|
|
50
|
+
|
|
51
|
+
const close = () => {
|
|
52
|
+
setInputMode(false)
|
|
53
|
+
setLocalLinkSite(null)
|
|
54
|
+
// If we arrived here from the "needs a local copy" report, return to it.
|
|
55
|
+
if (linkReturnToForgotten) {
|
|
56
|
+
setLinkReturnToForgotten(false)
|
|
57
|
+
setForgottenOpen(true)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const save = () => {
|
|
62
|
+
const path = pathInput.trim()
|
|
63
|
+
if (!site || !path) return // a path is required; the local URL is optional
|
|
64
|
+
linkSite(site.id, { domain: site.domain, path, localUrl: urlInput.trim() })
|
|
65
|
+
setMode("view")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const note = (msg: string) => {
|
|
69
|
+
setFlash(msg)
|
|
70
|
+
setTimeout(() => setFlash(null), 1800)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
useKeyboard((key) => {
|
|
74
|
+
const name = key.name ?? ""
|
|
75
|
+
|
|
76
|
+
if (mode === "edit") {
|
|
77
|
+
// Esc backs out: to the view mode if a link already exists, else closes.
|
|
78
|
+
if (name === "escape") return existing ? setMode("view") : close()
|
|
79
|
+
// ↑/↓ switch fields (Enter advances/saves via the inputs' onSubmit).
|
|
80
|
+
if (name === "up") return setField("path")
|
|
81
|
+
if (name === "down") return setField("url")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// view mode
|
|
86
|
+
switch (name) {
|
|
87
|
+
case "escape":
|
|
88
|
+
case "q":
|
|
89
|
+
return close()
|
|
90
|
+
case "t":
|
|
91
|
+
if (site) note(openLocalTerminal(site.id))
|
|
92
|
+
return
|
|
93
|
+
case "v":
|
|
94
|
+
if (site) note(openLocalUrl(site.id))
|
|
95
|
+
return
|
|
96
|
+
case "e":
|
|
97
|
+
setPathInput(existing?.path ?? "")
|
|
98
|
+
setUrlInput(existing?.localUrl ?? "")
|
|
99
|
+
setField("path")
|
|
100
|
+
return setMode("edit")
|
|
101
|
+
case "x":
|
|
102
|
+
if (site) unlinkSite(site.id)
|
|
103
|
+
return close()
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
if (!site) return null
|
|
108
|
+
|
|
109
|
+
const state = existing ? resolveLocalLink(existing) : null
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<box
|
|
113
|
+
style={{
|
|
114
|
+
position: "absolute",
|
|
115
|
+
top: 0,
|
|
116
|
+
left: 0,
|
|
117
|
+
width: "100%",
|
|
118
|
+
height: "100%",
|
|
119
|
+
flexDirection: "column",
|
|
120
|
+
backgroundColor: theme.bg,
|
|
121
|
+
zIndex: 210,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{/* Title bar */}
|
|
125
|
+
<box
|
|
126
|
+
style={{
|
|
127
|
+
flexDirection: "row",
|
|
128
|
+
height: 1,
|
|
129
|
+
backgroundColor: theme.bgAlt,
|
|
130
|
+
paddingLeft: 1,
|
|
131
|
+
paddingRight: 1,
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<text content="🔗 Local copy " fg={theme.brand} style={{ flexShrink: 0 }} />
|
|
136
|
+
<text content={truncate(site.domain, 40)} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
137
|
+
<box style={{ flexGrow: 1 }} />
|
|
138
|
+
<text content={existing ? "linked" : "not linked"} fg={existing ? theme.good : theme.textFaint} style={{ flexShrink: 0 }} />
|
|
139
|
+
</box>
|
|
140
|
+
|
|
141
|
+
<Centered>{mode === "edit" ? renderEdit() : renderView()}</Centered>
|
|
142
|
+
|
|
143
|
+
<StatusBar hints={hints()} message={flash ?? undefined} messageColor={theme.brand} showGlobal={false} />
|
|
144
|
+
</box>
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
function renderView() {
|
|
148
|
+
return (
|
|
149
|
+
<Panel title=" Linked working copy " active>
|
|
150
|
+
<box style={{ flexDirection: "column", width: 64, paddingTop: 1, paddingBottom: 1 }}>
|
|
151
|
+
<box style={{ flexDirection: "row" }}>
|
|
152
|
+
<text content="Status " fg={theme.textDim} style={{ flexShrink: 0 }} />
|
|
153
|
+
<text
|
|
154
|
+
content={state?.exists ? state.label : "missing — path not found"}
|
|
155
|
+
fg={kindColor(state?.kind ?? "unknown", state?.exists ?? false)}
|
|
156
|
+
wrapMode="none"
|
|
157
|
+
/>
|
|
158
|
+
</box>
|
|
159
|
+
<box style={{ flexDirection: "row" }}>
|
|
160
|
+
<text content="Path " fg={theme.textDim} style={{ flexShrink: 0 }} />
|
|
161
|
+
<text content={existing?.path ?? "—"} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
162
|
+
</box>
|
|
163
|
+
<box style={{ flexDirection: "row" }}>
|
|
164
|
+
<text content="URL " fg={theme.textDim} style={{ flexShrink: 0 }} />
|
|
165
|
+
<text content={existing?.localUrl || "—"} fg={existing?.localUrl ? theme.accent : theme.textFaint} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
166
|
+
</box>
|
|
167
|
+
<box style={{ height: 1 }} />
|
|
168
|
+
<text content="t — open a terminal here v — open the local URL" fg={theme.textDim} wrapMode="none" />
|
|
169
|
+
<text content="e — edit the link x — unlink" fg={theme.textDim} wrapMode="none" />
|
|
170
|
+
</box>
|
|
171
|
+
</Panel>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderEdit() {
|
|
176
|
+
return (
|
|
177
|
+
<Panel title={existing ? " Edit local link " : " Link a local copy "} active>
|
|
178
|
+
<box style={{ flexDirection: "column", width: 64, paddingTop: 1, paddingBottom: 1 }}>
|
|
179
|
+
<text content="Local path to the working copy (~ allowed)" fg={field === "path" ? theme.accent : theme.textDim} />
|
|
180
|
+
<input
|
|
181
|
+
focused={field === "path"}
|
|
182
|
+
value={pathInput}
|
|
183
|
+
placeholder="~/code/example.com"
|
|
184
|
+
onInput={setPathInput}
|
|
185
|
+
onSubmit={() => setField("url")}
|
|
186
|
+
style={{ backgroundColor: theme.bgAlt, focusedBackgroundColor: theme.bgAlt, textColor: theme.text }}
|
|
187
|
+
/>
|
|
188
|
+
<box style={{ height: 1 }} />
|
|
189
|
+
<text content="Local URL (optional) — Valet, Cove, LocalWP, Herd, DDEV…" fg={field === "url" ? theme.accent : theme.textDim} />
|
|
190
|
+
<input
|
|
191
|
+
focused={field === "url"}
|
|
192
|
+
value={urlInput}
|
|
193
|
+
placeholder="https://example.test"
|
|
194
|
+
onInput={setUrlInput}
|
|
195
|
+
onSubmit={save}
|
|
196
|
+
style={{ backgroundColor: theme.bgAlt, focusedBackgroundColor: theme.bgAlt, textColor: theme.text }}
|
|
197
|
+
/>
|
|
198
|
+
<box style={{ height: 1 }} />
|
|
199
|
+
<text content="↑↓ switch field · Enter on the URL saves · Esc cancels" fg={theme.textFaint} wrapMode="none" />
|
|
200
|
+
</box>
|
|
201
|
+
</Panel>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hints() {
|
|
206
|
+
if (mode === "edit")
|
|
207
|
+
return [
|
|
208
|
+
{ key: "↑↓", label: "field" },
|
|
209
|
+
{ key: "⏎", label: "next / save" },
|
|
210
|
+
{ key: "esc", label: "cancel" },
|
|
211
|
+
]
|
|
212
|
+
return [
|
|
213
|
+
{ key: "t", label: "terminal" },
|
|
214
|
+
{ key: "v", label: "local URL" },
|
|
215
|
+
{ key: "e", label: "edit" },
|
|
216
|
+
{ key: "x", label: "unlink" },
|
|
217
|
+
{ key: "esc", label: "close" },
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Production media fallback overlay.
|
|
2
|
+
//
|
|
3
|
+
// Opened with `m` on a linked WordPress site (Search), or from the `p` sync Done
|
|
4
|
+
// screen. Toggles a small mu-plugin in the local copy that serves missing-locally
|
|
5
|
+
// uploads from production — so a synced site's images resolve without pulling the
|
|
6
|
+
// whole media library. The plugin's presence IS the on/off state (no config), so
|
|
7
|
+
// this is just a file write/remove; nothing runs in the background.
|
|
8
|
+
|
|
9
|
+
import { useState } from "react"
|
|
10
|
+
import { useKeyboard } from "@opentui/react"
|
|
11
|
+
import { theme } from "../../lib/theme.ts"
|
|
12
|
+
import { truncate } from "../../lib/format.ts"
|
|
13
|
+
import { Panel, Centered, DestPath, Sparkle } from "../components.tsx"
|
|
14
|
+
import { StatusBar } from "../StatusBar.tsx"
|
|
15
|
+
import { useStore } from "../store.tsx"
|
|
16
|
+
import { enableMediaFallback, disableMediaFallback } from "../../lib/mediaFallback.ts"
|
|
17
|
+
|
|
18
|
+
export function MediaFallback() {
|
|
19
|
+
const { mediaFallbackSite: site, setMediaFallbackSite, planMediaFallbackFor } = useStore()
|
|
20
|
+
// Re-resolve every render so the panel reflects the file on disk after a toggle.
|
|
21
|
+
const planResult = site ? planMediaFallbackFor(site) : null
|
|
22
|
+
const [changed, setChanged] = useState<"enabled" | "disabled" | "updated" | null>(null)
|
|
23
|
+
const [error, setError] = useState<string | null>(null)
|
|
24
|
+
|
|
25
|
+
const plan = planResult && planResult.ok ? planResult.plan : null
|
|
26
|
+
|
|
27
|
+
const close = () => setMediaFallbackSite(null)
|
|
28
|
+
|
|
29
|
+
useKeyboard((key) => {
|
|
30
|
+
const name = key.name ?? ""
|
|
31
|
+
if (name === "escape" || name === "q") return close()
|
|
32
|
+
if (!plan) return
|
|
33
|
+
try {
|
|
34
|
+
if (!plan.enabled && (name === "return" || name === "y")) {
|
|
35
|
+
enableMediaFallback(plan)
|
|
36
|
+
setChanged("enabled")
|
|
37
|
+
setError(null)
|
|
38
|
+
} else if (plan.enabled && plan.updateAvailable && name === "u") {
|
|
39
|
+
enableMediaFallback(plan) // overwrite in place with the current version
|
|
40
|
+
setChanged("updated")
|
|
41
|
+
setError(null)
|
|
42
|
+
} else if (plan.enabled && name === "x") {
|
|
43
|
+
disableMediaFallback(plan)
|
|
44
|
+
setChanged("disabled")
|
|
45
|
+
setError(null)
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
setError(e instanceof Error ? e.message : "Couldn't update the media-fallback plugin.")
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!site) return null
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<box
|
|
56
|
+
style={{
|
|
57
|
+
position: "absolute",
|
|
58
|
+
top: 0,
|
|
59
|
+
left: 0,
|
|
60
|
+
width: "100%",
|
|
61
|
+
height: "100%",
|
|
62
|
+
flexDirection: "column",
|
|
63
|
+
backgroundColor: theme.bg,
|
|
64
|
+
zIndex: 210,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<box style={{ flexDirection: "row", height: 1, backgroundColor: theme.bgAlt, paddingLeft: 1, paddingRight: 1, alignItems: "center" }}>
|
|
68
|
+
<text content="🖼 Production media fallback " fg={theme.brand} style={{ flexShrink: 0 }} />
|
|
69
|
+
<text content={truncate(site.domain, 38)} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
70
|
+
<box style={{ flexGrow: 1 }} />
|
|
71
|
+
<text content="local only" fg={theme.textFaint} style={{ flexShrink: 0 }} />
|
|
72
|
+
</box>
|
|
73
|
+
|
|
74
|
+
<Centered>{renderBody()}</Centered>
|
|
75
|
+
|
|
76
|
+
<StatusBar hints={hints()} showGlobal={false} />
|
|
77
|
+
</box>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
function renderBody() {
|
|
81
|
+
if (!plan) {
|
|
82
|
+
return (
|
|
83
|
+
<Panel title=" Can't set up media fallback " active>
|
|
84
|
+
<box style={{ flexDirection: "column", width: 66, paddingTop: 1, paddingBottom: 1 }}>
|
|
85
|
+
<text content={planResult && !planResult.ok ? planResult.error : "Unavailable."} fg={theme.warn} wrapMode="none" />
|
|
86
|
+
<box style={{ height: 1 }} />
|
|
87
|
+
<text content="Esc to close" fg={theme.textFaint} />
|
|
88
|
+
</box>
|
|
89
|
+
</Panel>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Panel title=" Serve missing images from production " active>
|
|
95
|
+
<box style={{ flexDirection: "column", width: 68, paddingTop: 1, paddingBottom: 1 }}>
|
|
96
|
+
{plan.enabled && plan.updateAvailable && changed !== "updated" ? (
|
|
97
|
+
<box
|
|
98
|
+
title=" ✨ Update available "
|
|
99
|
+
titleColor={theme.brand}
|
|
100
|
+
border
|
|
101
|
+
borderColor={theme.brand}
|
|
102
|
+
style={{ flexDirection: "column", width: 66, paddingLeft: 1, paddingRight: 1, marginBottom: 1 }}
|
|
103
|
+
>
|
|
104
|
+
<box style={{ flexDirection: "row" }}>
|
|
105
|
+
<Sparkle />
|
|
106
|
+
<text content={` Newer missing-images plugin ready (v${plan.installedVersion} → v${plan.version}).`} fg={theme.text} wrapMode="none" />
|
|
107
|
+
</box>
|
|
108
|
+
<text content="Now covers legacy /wp-content/uploads paths, CDN/S3" fg={theme.textDim} wrapMode="none" />
|
|
109
|
+
<text content="redirects, and Elementor gallery URLs." fg={theme.textDim} wrapMode="none" />
|
|
110
|
+
<box style={{ flexDirection: "row" }}>
|
|
111
|
+
<text content="Press " fg={theme.textDim} />
|
|
112
|
+
<text content="u" fg={theme.brand} />
|
|
113
|
+
<text content=" to update in place." fg={theme.textDim} />
|
|
114
|
+
</box>
|
|
115
|
+
</box>
|
|
116
|
+
) : null}
|
|
117
|
+
<text content="When an upload is missing locally, its URL is rewritten to" fg={theme.textDim} wrapMode="none" />
|
|
118
|
+
<text content="production — so images resolve without syncing the library." fg={theme.textDim} wrapMode="none" />
|
|
119
|
+
<box style={{ height: 1 }} />
|
|
120
|
+
<box style={{ flexDirection: "row" }}>
|
|
121
|
+
<text content="source " fg={theme.textFaint} />
|
|
122
|
+
<text content={plan.prodOrigin} fg={theme.accent} wrapMode="none" />
|
|
123
|
+
<text content={` (${plan.stack === "bedrock" ? "Bedrock" : "Standard WP"})`} fg={theme.textFaint} wrapMode="none" />
|
|
124
|
+
</box>
|
|
125
|
+
<text content="plugin" fg={theme.textFaint} />
|
|
126
|
+
<DestPath path={plan.pluginPath} fileColor={plan.enabled ? theme.good : theme.textDim} width={64} />
|
|
127
|
+
<box style={{ height: 1 }} />
|
|
128
|
+
{renderStatus()}
|
|
129
|
+
<box style={{ height: 1 }} />
|
|
130
|
+
<text content="Needs production reachable + hotlinking allowed (no staging" fg={theme.textFaint} wrapMode="none" />
|
|
131
|
+
<text content="Basic-Auth / hotlink protection). Read-only on production." fg={theme.textFaint} wrapMode="none" />
|
|
132
|
+
</box>
|
|
133
|
+
</Panel>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderStatus() {
|
|
138
|
+
if (error) return <text content={`✕ ${error}`} fg={theme.bad} />
|
|
139
|
+
if (plan!.enabled) {
|
|
140
|
+
const note =
|
|
141
|
+
changed === "updated"
|
|
142
|
+
? ` — updated to v${plan!.version}. Reload the local site.`
|
|
143
|
+
: changed === "enabled"
|
|
144
|
+
? " — enabled. Reload the local site; images now load from production."
|
|
145
|
+
: " — missing images load from production."
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<box style={{ flexDirection: "row" }}>
|
|
149
|
+
<text content="● ON" fg={theme.good} />
|
|
150
|
+
<text content={note} fg={theme.text} wrapMode="none" />
|
|
151
|
+
</box>
|
|
152
|
+
<text content="Press x to turn off" fg={theme.textFaint} />
|
|
153
|
+
</>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
return (
|
|
157
|
+
<>
|
|
158
|
+
<box style={{ flexDirection: "row" }}>
|
|
159
|
+
<text content="○ OFF" fg={theme.textDim} />
|
|
160
|
+
<text content={changed === "disabled" ? " — removed. Missing images will 404 locally again." : " — missing images 404 locally."} fg={theme.text} wrapMode="none" />
|
|
161
|
+
</box>
|
|
162
|
+
<text content="Press y / ⏎ to turn on" fg={theme.textFaint} />
|
|
163
|
+
</>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hints() {
|
|
168
|
+
if (!plan) return [{ key: "esc", label: "close" }]
|
|
169
|
+
if (!plan.enabled)
|
|
170
|
+
return [
|
|
171
|
+
{ key: "y/⏎", label: "turn on" },
|
|
172
|
+
{ key: "esc", label: "close" },
|
|
173
|
+
]
|
|
174
|
+
return [
|
|
175
|
+
...(plan.updateAvailable && changed !== "updated" ? [{ key: "u", label: "update" }] : []),
|
|
176
|
+
{ key: "x", label: "turn off" },
|
|
177
|
+
{ key: "esc", label: "close" },
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
}
|