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,186 @@
|
|
|
1
|
+
// Purge page cache + object cache overlay — mirrors HttpsToggle.tsx's
|
|
2
|
+
// confirm→tracking→done/error shape. There's no enable/disable for either
|
|
3
|
+
// cache on an existing site (only at site-creation time, per the SpinupWP
|
|
4
|
+
// API), so purge is the only available write; one confirm fires both
|
|
5
|
+
// purge-page-cache and purge-object-cache together. Opened with `P` (capital
|
|
6
|
+
// — lowercase `p` is already the DB-pull action in Search) on a selected site.
|
|
7
|
+
//
|
|
8
|
+
// The writes + event polling live in the store (startPurgeCache), so closing
|
|
9
|
+
// this overlay (Esc/q) doesn't abandon them.
|
|
10
|
+
|
|
11
|
+
import { useState } from "react"
|
|
12
|
+
import { useKeyboard } from "@opentui/react"
|
|
13
|
+
import { theme } from "../../lib/theme.ts"
|
|
14
|
+
import { truncate } from "../../lib/format.ts"
|
|
15
|
+
import { Panel, Spinner, Centered } from "../components.tsx"
|
|
16
|
+
import { StatusBar } from "../StatusBar.tsx"
|
|
17
|
+
import { useStore, isPurgeCacheInFlight, purgeCacheFailed } from "../store.tsx"
|
|
18
|
+
|
|
19
|
+
type Phase = "confirm" | "tracking" | "done" | "error"
|
|
20
|
+
|
|
21
|
+
function subLabel(status: string): string {
|
|
22
|
+
return status === "deployed" ? "done" : status === "failed" ? "failed" : status || "queued"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PurgeCache() {
|
|
26
|
+
const store = useStore()
|
|
27
|
+
const { purgeCacheSite: site, setPurgeCacheSite, purgeCacheProgress, startPurgeCache, clearPurgeCache } = store
|
|
28
|
+
|
|
29
|
+
const [phase, setPhase] = useState<Phase>("confirm")
|
|
30
|
+
const progress = site ? purgeCacheProgress.get(site.id) : undefined
|
|
31
|
+
const dp: Phase =
|
|
32
|
+
phase !== "tracking"
|
|
33
|
+
? phase
|
|
34
|
+
: !progress
|
|
35
|
+
? "tracking"
|
|
36
|
+
: purgeCacheFailed(progress)
|
|
37
|
+
? "error"
|
|
38
|
+
: isPurgeCacheInFlight(progress)
|
|
39
|
+
? "tracking"
|
|
40
|
+
: "done"
|
|
41
|
+
|
|
42
|
+
const close = () => {
|
|
43
|
+
if (site && purgeCacheFailed(progress)) clearPurgeCache(site.id)
|
|
44
|
+
setPurgeCacheSite(null)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
useKeyboard((key) => {
|
|
48
|
+
const name = key.name ?? ""
|
|
49
|
+
if (name === "escape" || name === "q") return close()
|
|
50
|
+
|
|
51
|
+
if (dp === "confirm") {
|
|
52
|
+
if (name === "y") {
|
|
53
|
+
if (site) {
|
|
54
|
+
startPurgeCache(site)
|
|
55
|
+
setPhase("tracking")
|
|
56
|
+
}
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (dp === "error") {
|
|
63
|
+
if (name === "r") {
|
|
64
|
+
if (site) clearPurgeCache(site.id)
|
|
65
|
+
setPhase("confirm")
|
|
66
|
+
}
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (!site) return null
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<box
|
|
75
|
+
style={{
|
|
76
|
+
position: "absolute",
|
|
77
|
+
top: 0,
|
|
78
|
+
left: 0,
|
|
79
|
+
width: "100%",
|
|
80
|
+
height: "100%",
|
|
81
|
+
flexDirection: "column",
|
|
82
|
+
backgroundColor: theme.bg,
|
|
83
|
+
zIndex: 210,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<box
|
|
87
|
+
style={{
|
|
88
|
+
flexDirection: "row",
|
|
89
|
+
height: 1,
|
|
90
|
+
backgroundColor: theme.bgAlt,
|
|
91
|
+
paddingLeft: 1,
|
|
92
|
+
paddingRight: 1,
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<text content="🗑 Purge cache " fg={theme.brand} style={{ flexShrink: 0 }} />
|
|
97
|
+
<text content={truncate(site.domain, 40)} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
98
|
+
</box>
|
|
99
|
+
|
|
100
|
+
<Centered>{renderBody()}</Centered>
|
|
101
|
+
|
|
102
|
+
<StatusBar hints={hints()} showGlobal={false} />
|
|
103
|
+
</box>
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
function renderBody() {
|
|
107
|
+
if (dp === "confirm") {
|
|
108
|
+
return (
|
|
109
|
+
<Panel title=" Confirm: purge cache " active>
|
|
110
|
+
<box style={{ flexDirection: "column", width: 60, paddingTop: 1, paddingBottom: 1 }}>
|
|
111
|
+
<box style={{ flexDirection: "row" }}>
|
|
112
|
+
<text content="Purge page cache + object cache for " fg={theme.text} wrapMode="none" />
|
|
113
|
+
</box>
|
|
114
|
+
<text content={truncate(site!.domain, 32)} fg={theme.accent} wrapMode="none" />
|
|
115
|
+
<box style={{ height: 1 }} />
|
|
116
|
+
<text content="Clears both caches; SpinupWP rebuilds them on the next" fg={theme.textDim} wrapMode="none" />
|
|
117
|
+
<text content="page load. Low-risk and fully reversible by nature." fg={theme.textDim} wrapMode="none" />
|
|
118
|
+
<box style={{ height: 1 }} />
|
|
119
|
+
<text content="Press y to confirm · Esc to cancel" fg={theme.textFaint} wrapMode="none" />
|
|
120
|
+
</box>
|
|
121
|
+
</Panel>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (dp === "tracking") {
|
|
126
|
+
return (
|
|
127
|
+
<box style={{ flexDirection: "column", alignItems: "center" }}>
|
|
128
|
+
<box style={{ flexDirection: "row" }}>
|
|
129
|
+
<Spinner />
|
|
130
|
+
<text content=" Purging…" fg={theme.textDim} />
|
|
131
|
+
</box>
|
|
132
|
+
<box style={{ height: 1 }} />
|
|
133
|
+
<text content={`Page cache: ${subLabel(progress?.page.status ?? "queued")}`} fg={theme.textDim} wrapMode="none" />
|
|
134
|
+
<text content={`Object cache: ${subLabel(progress?.object.status ?? "queued")}`} fg={theme.textDim} wrapMode="none" />
|
|
135
|
+
<box style={{ height: 1 }} />
|
|
136
|
+
<text content="You can press Esc — it keeps running in the background." fg={theme.textFaint} wrapMode="none" />
|
|
137
|
+
</box>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (dp === "done") {
|
|
142
|
+
return (
|
|
143
|
+
<Panel title=" Done " active>
|
|
144
|
+
<box style={{ flexDirection: "column", width: 56, paddingTop: 1, paddingBottom: 1 }}>
|
|
145
|
+
<box style={{ flexDirection: "row" }}>
|
|
146
|
+
<text content="✓ " fg={theme.good} />
|
|
147
|
+
<text content={truncate(site!.domain, 32)} fg={theme.accent} wrapMode="none" />
|
|
148
|
+
<text content=" — cache purged" fg={theme.text} wrapMode="none" />
|
|
149
|
+
</box>
|
|
150
|
+
<box style={{ height: 1 }} />
|
|
151
|
+
<text content="Esc to close" fg={theme.textFaint} />
|
|
152
|
+
</box>
|
|
153
|
+
</Panel>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// error
|
|
158
|
+
return (
|
|
159
|
+
<Panel title=" Failed " active>
|
|
160
|
+
<box style={{ flexDirection: "column", width: 66, paddingTop: 1, paddingBottom: 1 }}>
|
|
161
|
+
<text content={`✕ Page cache: ${progress?.page.error ?? subLabel(progress?.page.status ?? "")}`} fg={theme.bad} wrapMode="none" />
|
|
162
|
+
<text content={`✕ Object cache: ${progress?.object.error ?? subLabel(progress?.object.status ?? "")}`} fg={theme.bad} wrapMode="none" />
|
|
163
|
+
<box style={{ height: 1 }} />
|
|
164
|
+
<text content="Press r to try again · Esc to close" fg={theme.textFaint} wrapMode="none" />
|
|
165
|
+
</box>
|
|
166
|
+
</Panel>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hints() {
|
|
171
|
+
switch (dp) {
|
|
172
|
+
case "confirm":
|
|
173
|
+
return [
|
|
174
|
+
{ key: "y", label: "confirm" },
|
|
175
|
+
{ key: "esc", label: "cancel" },
|
|
176
|
+
]
|
|
177
|
+
case "error":
|
|
178
|
+
return [
|
|
179
|
+
{ key: "r", label: "retry" },
|
|
180
|
+
{ key: "esc", label: "close" },
|
|
181
|
+
]
|
|
182
|
+
default:
|
|
183
|
+
return [{ key: "esc", label: "close" }]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// "What's new" overlay — shows once after Spinup updates to a new version,
|
|
2
|
+
// rendering the GitHub release notes for the exact version now running (see
|
|
3
|
+
// ../../lib/releaseNotes.ts). Auto-opens when store.releaseNotesInfo is set
|
|
4
|
+
// (no keypress needed to open it); dismissed with any key, which persists the
|
|
5
|
+
// running version as "seen" so it never shows again for this version.
|
|
6
|
+
|
|
7
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react"
|
|
8
|
+
import { theme } from "../../lib/theme.ts"
|
|
9
|
+
import { formatReleaseNotesBody, type NoteLine } from "../../lib/releaseNotes.ts"
|
|
10
|
+
import { useStore } from "../store.tsx"
|
|
11
|
+
|
|
12
|
+
// Rough estimate of how many VISUAL rows a line will wrap to at a given
|
|
13
|
+
// content width — needed because the box's height must be based on wrapped
|
|
14
|
+
// rows, not logical lines (a single long bullet can span 3-4 rows).
|
|
15
|
+
function estimatedRows(content: string, width: number): number {
|
|
16
|
+
return Math.max(1, Math.ceil(content.length / Math.max(1, width)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ReleaseNotes() {
|
|
20
|
+
const { releaseNotesInfo, dismissReleaseNotes } = useStore()
|
|
21
|
+
const { width, height } = useTerminalDimensions()
|
|
22
|
+
|
|
23
|
+
// One-shot announcement, not a navigable view — any key closes it.
|
|
24
|
+
useKeyboard(() => dismissReleaseNotes())
|
|
25
|
+
|
|
26
|
+
if (!releaseNotesInfo) return null
|
|
27
|
+
|
|
28
|
+
const boxWidth = Math.min(width - 4, 100)
|
|
29
|
+
const contentWidth = boxWidth - 6 // border(2) + paddingLeft/Right(2+2)
|
|
30
|
+
const maxVisualRows = Math.max(8, height - 8)
|
|
31
|
+
|
|
32
|
+
// Fit as many logical lines as fit within maxVisualRows of WRAPPED content —
|
|
33
|
+
// the box itself is left unsized (grows to fit, like every other overlay in
|
|
34
|
+
// this app) rather than pre-computed, which is what caused the overlap bug:
|
|
35
|
+
// a fixed height sized to logical-line count was too short once bullets
|
|
36
|
+
// actually word-wrapped, so later lines drew over still-wrapping earlier ones.
|
|
37
|
+
let usedRows = 0
|
|
38
|
+
let truncated = false
|
|
39
|
+
const lines: NoteLine[] = []
|
|
40
|
+
for (const line of formatReleaseNotesBody(releaseNotesInfo.body)) {
|
|
41
|
+
const w = line.kind === "bullet" ? contentWidth - 2 : contentWidth
|
|
42
|
+
const rows = line.kind === "blank" ? 1 : estimatedRows(line.content, w)
|
|
43
|
+
if (usedRows + rows > maxVisualRows) {
|
|
44
|
+
truncated = true
|
|
45
|
+
break
|
|
46
|
+
}
|
|
47
|
+
usedRows += rows
|
|
48
|
+
lines.push(line)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<box
|
|
53
|
+
style={{
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top: 0,
|
|
56
|
+
left: 0,
|
|
57
|
+
width: "100%",
|
|
58
|
+
height: "100%",
|
|
59
|
+
justifyContent: "center",
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
zIndex: 100,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<box
|
|
65
|
+
title={` ✦ What's new in ${releaseNotesInfo.title} `}
|
|
66
|
+
titleColor={theme.update}
|
|
67
|
+
bottomTitle=" press any key to close "
|
|
68
|
+
bottomTitleAlignment="center"
|
|
69
|
+
border
|
|
70
|
+
borderColor={theme.borderActive}
|
|
71
|
+
backgroundColor={theme.bgPanel}
|
|
72
|
+
style={{ flexDirection: "column", width: boxWidth, paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1 }}
|
|
73
|
+
>
|
|
74
|
+
{lines.map((line, i) => {
|
|
75
|
+
if (line.kind === "blank") return <box key={i} style={{ height: 1 }} />
|
|
76
|
+
if (line.kind === "heading") return <text key={i} content={line.content} fg={theme.accent} attributes={1} />
|
|
77
|
+
if (line.kind === "bullet") return <text key={i} content={"• " + line.content} fg={theme.text} />
|
|
78
|
+
return <text key={i} content={line.content} fg={theme.text} />
|
|
79
|
+
})}
|
|
80
|
+
{truncated ? <text content="…" fg={theme.textFaint} /> : null}
|
|
81
|
+
</box>
|
|
82
|
+
</box>
|
|
83
|
+
)
|
|
84
|
+
}
|