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.
Files changed (88) hide show
  1. package/CHANGELOG.md +943 -0
  2. package/LICENSE +21 -0
  3. package/README.md +856 -0
  4. package/bin/spinuptui.js +13 -0
  5. package/package.json +55 -0
  6. package/src/api/client.ts +354 -0
  7. package/src/api/types.ts +206 -0
  8. package/src/config.ts +309 -0
  9. package/src/dev/devMode.ts +7 -0
  10. package/src/dev/fixtures.ts +238 -0
  11. package/src/dev/mockClient.ts +214 -0
  12. package/src/index.tsx +68 -0
  13. package/src/lib/appUpdate.ts +206 -0
  14. package/src/lib/awsSigV4.ts +133 -0
  15. package/src/lib/cloneDomains.ts +112 -0
  16. package/src/lib/cloneLog.ts +85 -0
  17. package/src/lib/dbBackup.ts +198 -0
  18. package/src/lib/dbSync.ts +253 -0
  19. package/src/lib/discover.ts +165 -0
  20. package/src/lib/dns.ts +144 -0
  21. package/src/lib/dnsCache.ts +94 -0
  22. package/src/lib/dnsQuery.ts +180 -0
  23. package/src/lib/dnsRecords.ts +411 -0
  24. package/src/lib/format.ts +106 -0
  25. package/src/lib/gitDeployKey.ts +0 -0
  26. package/src/lib/gitStatus.ts +37 -0
  27. package/src/lib/jobs.ts +32 -0
  28. package/src/lib/keychain.ts +49 -0
  29. package/src/lib/local.ts +116 -0
  30. package/src/lib/mediaFallback.ts +288 -0
  31. package/src/lib/open.ts +103 -0
  32. package/src/lib/phpEol.ts +151 -0
  33. package/src/lib/probe.ts +183 -0
  34. package/src/lib/providers.ts +284 -0
  35. package/src/lib/providersCache.ts +105 -0
  36. package/src/lib/releaseNotes.ts +67 -0
  37. package/src/lib/serverClone.ts +782 -0
  38. package/src/lib/serverCreate.ts +161 -0
  39. package/src/lib/siteDoctor.ts +260 -0
  40. package/src/lib/siteFingerprint.ts +108 -0
  41. package/src/lib/spinupweb.ts +33 -0
  42. package/src/lib/ssh.ts +669 -0
  43. package/src/lib/stack.ts +78 -0
  44. package/src/lib/stackCache.ts +113 -0
  45. package/src/lib/theme.ts +54 -0
  46. package/src/lib/ubuntuEol.ts +124 -0
  47. package/src/lib/uptimeKuma.ts +551 -0
  48. package/src/lib/vanitySite.ts +297 -0
  49. package/src/lib/wpInventory.ts +153 -0
  50. package/src/ui/App.tsx +265 -0
  51. package/src/ui/Details.tsx +403 -0
  52. package/src/ui/Explain.tsx +158 -0
  53. package/src/ui/Header.tsx +117 -0
  54. package/src/ui/Help.tsx +296 -0
  55. package/src/ui/List.tsx +95 -0
  56. package/src/ui/Onboarding.tsx +93 -0
  57. package/src/ui/Splash.tsx +51 -0
  58. package/src/ui/StatusBar.tsx +60 -0
  59. package/src/ui/components.tsx +392 -0
  60. package/src/ui/store.tsx +4253 -0
  61. package/src/ui/views/Browser.tsx +365 -0
  62. package/src/ui/views/CloneWizard.tsx +932 -0
  63. package/src/ui/views/Dashboard.tsx +149 -0
  64. package/src/ui/views/DbBackup.tsx +228 -0
  65. package/src/ui/views/DbSync.tsx +307 -0
  66. package/src/ui/views/Discover.tsx +271 -0
  67. package/src/ui/views/DnsInventory.tsx +580 -0
  68. package/src/ui/views/DnsRecords.tsx +562 -0
  69. package/src/ui/views/Events.tsx +98 -0
  70. package/src/ui/views/Forgotten.tsx +165 -0
  71. package/src/ui/views/GrantKey.tsx +504 -0
  72. package/src/ui/views/Health.tsx +322 -0
  73. package/src/ui/views/HttpsToggle.tsx +195 -0
  74. package/src/ui/views/KumaSite.tsx +571 -0
  75. package/src/ui/views/LocalLink.tsx +220 -0
  76. package/src/ui/views/MediaFallback.tsx +180 -0
  77. package/src/ui/views/NewServer.tsx +750 -0
  78. package/src/ui/views/PhpUpgrade.tsx +301 -0
  79. package/src/ui/views/ProviderConnect.tsx +527 -0
  80. package/src/ui/views/PurgeCache.tsx +186 -0
  81. package/src/ui/views/ReleaseNotes.tsx +84 -0
  82. package/src/ui/views/Search.tsx +448 -0
  83. package/src/ui/views/ServerActions.tsx +328 -0
  84. package/src/ui/views/Stacks.tsx +371 -0
  85. package/src/ui/views/SudoConnect.tsx +358 -0
  86. package/src/ui/views/VanityNewSite.tsx +407 -0
  87. package/src/ui/views/WpInventory.tsx +218 -0
  88. package/src/version.ts +8 -0
@@ -0,0 +1,301 @@
1
+ // PHP-version upgrade overlay — the app's first *write* action.
2
+ //
3
+ // Opened with `u` on a selected site (Browser / Stacks). Walks through: pick a
4
+ // version → confirm → fire the upgrade. The actual PUT + event polling live in
5
+ // the store (`startPhpUpgrade`), so closing this modal (Esc/q) doesn't abandon
6
+ // the upgrade — the site's row keeps a spinner until it settles. A site whose
7
+ // server has a pending SpinupWP platform upgrade is blocked up front (the API
8
+ // can't manage it until that runs — point the user at the `w` deep link).
9
+
10
+ import { useState } from "react"
11
+ import { useKeyboard } from "@opentui/react"
12
+ import { theme } from "../../lib/theme.ts"
13
+ import { truncate } from "../../lib/format.ts"
14
+ import { Panel, Spinner, Centered } from "../components.tsx"
15
+ import { StatusBar } from "../StatusBar.tsx"
16
+ import { useStore } from "../store.tsx"
17
+ import { moveSelection } from "../List.tsx"
18
+
19
+ type Phase = "blocked" | "pick" | "confirm" | "tracking" | "done" | "error"
20
+
21
+ function majorMinor(v: string | null | undefined): string {
22
+ if (!v) return "—"
23
+ const [maj, min] = v.split(".")
24
+ return min != null ? `${maj}.${min}` : maj
25
+ }
26
+
27
+ export function PhpUpgrade() {
28
+ const store = useStore()
29
+ const {
30
+ phpUpgradeSite: site,
31
+ setPhpUpgradeSite,
32
+ serverById,
33
+ offeredPhpVersions,
34
+ isPhpEol,
35
+ accountSlug,
36
+ phpUpgrades,
37
+ startPhpUpgrade,
38
+ clearPhpUpgrade,
39
+ } = store
40
+
41
+ const server = serverById(site?.server_id)
42
+ const versions = site ? offeredPhpVersions(site.php_version) : []
43
+ const current = majorMinor(site?.php_version)
44
+
45
+ // Start blocked if the server can't be managed via the API yet.
46
+ const [phase, setPhase] = useState<Phase>(() => (server?.upgrade_required ? "blocked" : "pick"))
47
+ // Open the cursor on the current version (orients the user) when present.
48
+ const [index, setIndex] = useState(() => {
49
+ const i = versions.indexOf(current)
50
+ return i >= 0 ? i : 0
51
+ })
52
+ const [target, setTarget] = useState<string | null>(null)
53
+
54
+ // Background progress for this site, owned by the store. Once we've fired the
55
+ // upgrade ("tracking"), the screen follows the store's event status: a settled
56
+ // failure shows the error; a cleared entry (deleted on deploy) means success.
57
+ const progress = site ? phpUpgrades.get(site.id) : undefined
58
+ const dp: Phase =
59
+ phase !== "tracking"
60
+ ? phase
61
+ : !progress
62
+ ? "done"
63
+ : progress.status === "failed"
64
+ ? "error"
65
+ : "tracking"
66
+
67
+ const close = () => {
68
+ // Leave an in-flight upgrade running (its row keeps the spinner); only drop a
69
+ // settled failure so its marker doesn't linger after the user has seen it.
70
+ if (site && progress?.status === "failed") clearPhpUpgrade(site.id)
71
+ setPhpUpgradeSite(null)
72
+ }
73
+
74
+ useKeyboard((key) => {
75
+ const name = key.name ?? ""
76
+ // Esc/q always closes — the upgrade keeps running in the background.
77
+ if (name === "escape" || name === "q") return close()
78
+
79
+ if (dp === "pick") {
80
+ switch (name) {
81
+ case "up":
82
+ case "k":
83
+ return setIndex((i) => moveSelection(i, -1, versions.length))
84
+ case "down":
85
+ case "j":
86
+ return setIndex((i) => moveSelection(i, 1, versions.length))
87
+ case "return":
88
+ case "right":
89
+ case "l": {
90
+ const v = versions[index]
91
+ if (!v || v === current) return // no-op on the current version
92
+ setTarget(v)
93
+ setPhase("confirm")
94
+ return
95
+ }
96
+ }
97
+ return
98
+ }
99
+
100
+ if (dp === "confirm") {
101
+ if (name === "y") {
102
+ if (site && target) {
103
+ startPhpUpgrade(site, target)
104
+ setPhase("tracking")
105
+ }
106
+ return
107
+ }
108
+ if (name === "left" || name === "h") return setPhase("pick")
109
+ return
110
+ }
111
+
112
+ if (dp === "error") {
113
+ if (name === "r") {
114
+ if (site) clearPhpUpgrade(site.id)
115
+ setPhase("pick")
116
+ }
117
+ return
118
+ }
119
+ })
120
+
121
+ if (!site) return null
122
+
123
+ return (
124
+ <box
125
+ style={{
126
+ position: "absolute",
127
+ top: 0,
128
+ left: 0,
129
+ width: "100%",
130
+ height: "100%",
131
+ flexDirection: "column",
132
+ backgroundColor: theme.bg,
133
+ zIndex: 210,
134
+ }}
135
+ >
136
+ {/* Title bar */}
137
+ <box
138
+ style={{
139
+ flexDirection: "row",
140
+ height: 1,
141
+ backgroundColor: theme.bgAlt,
142
+ paddingLeft: 1,
143
+ paddingRight: 1,
144
+ alignItems: "center",
145
+ }}
146
+ >
147
+ <text content="⬆ Upgrade PHP " fg={theme.brand} style={{ flexShrink: 0 }} />
148
+ <text content={truncate(site.domain, 40)} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
149
+ <box style={{ flexGrow: 1 }} />
150
+ <text content={`current PHP ${current}`} fg={theme.textFaint} style={{ flexShrink: 0 }} />
151
+ </box>
152
+
153
+ <Centered>{renderBody()}</Centered>
154
+
155
+ <StatusBar hints={hints()} showGlobal={false} />
156
+ </box>
157
+ )
158
+
159
+ function renderBody() {
160
+ if (dp === "blocked") {
161
+ return (
162
+ <Panel title=" Can't upgrade yet " active>
163
+ <box style={{ flexDirection: "column", width: 64, paddingTop: 1, paddingBottom: 1 }}>
164
+ <text content="This site's server has a pending SpinupWP platform upgrade." fg={theme.warn} wrapMode="none" />
165
+ <text content="SpinupWP can't manage the site over the API until that runs." fg={theme.textDim} wrapMode="none" />
166
+ <box style={{ height: 1 }} />
167
+ <text
168
+ content={
169
+ accountSlug
170
+ ? "Press w in the Servers view to open the server in SpinupWP."
171
+ : "Open the server in SpinupWP to run the upgrade (set accountSlug for a deep link)."
172
+ }
173
+ fg={theme.textDim}
174
+ wrapMode="none"
175
+ />
176
+ <box style={{ height: 1 }} />
177
+ <text content="Esc to close" fg={theme.textFaint} />
178
+ </box>
179
+ </Panel>
180
+ )
181
+ }
182
+
183
+ if (dp === "pick") {
184
+ return (
185
+ <Panel title=" Choose a PHP version " active>
186
+ <box style={{ flexDirection: "column", width: 48 }}>
187
+ {versions.map((v, i) => {
188
+ const selected = i === index
189
+ const isCurrent = v === current
190
+ const eol = isPhpEol(v)
191
+ // On the highlighted row, force bright text so it stays legible on
192
+ // the green selection background (same approach as List/Stacks).
193
+ const baseFg = isCurrent ? theme.textFaint : eol ? theme.bad : theme.textDim
194
+ const fg = selected ? theme.text : baseFg
195
+ const tag = isCurrent ? " (current)" : eol ? " (EOL)" : ""
196
+ const tagFg = selected ? theme.text : isCurrent ? theme.textFaint : theme.bad
197
+ return (
198
+ <box
199
+ key={v}
200
+ style={{ flexDirection: "row", height: 1, backgroundColor: selected ? theme.selectedBg : undefined }}
201
+ >
202
+ <text content={(selected ? "❯ " : " ") + `PHP ${v}`} fg={fg} style={{ flexGrow: 1 }} wrapMode="none" />
203
+ <text content={tag} fg={tagFg} style={{ flexShrink: 0 }} />
204
+ </box>
205
+ )
206
+ })}
207
+ <box style={{ height: 1 }} />
208
+ <text content="A version not on the server is installed first." fg={theme.textFaint} wrapMode="none" />
209
+ </box>
210
+ </Panel>
211
+ )
212
+ }
213
+
214
+ if (dp === "confirm") {
215
+ return (
216
+ <Panel title=" Confirm upgrade " active>
217
+ <box style={{ flexDirection: "column", width: 60, paddingTop: 1, paddingBottom: 1 }}>
218
+ <box style={{ flexDirection: "row" }}>
219
+ <text content="Upgrade " fg={theme.text} />
220
+ <text content={truncate(site!.domain, 32)} fg={theme.accent} wrapMode="none" />
221
+ </box>
222
+ <box style={{ flexDirection: "row" }}>
223
+ <text content={`from PHP ${current}`} fg={theme.textDim} />
224
+ <text content=" → " fg={theme.textFaint} />
225
+ <text content={`PHP ${target}`} fg={theme.good} />
226
+ </box>
227
+ <box style={{ height: 1 }} />
228
+ <text content="SpinupWP restarts PHP-FPM (and installs the version first" fg={theme.textDim} wrapMode="none" />
229
+ <text content="if it isn't present on the server yet)." fg={theme.textDim} wrapMode="none" />
230
+ <box style={{ height: 1 }} />
231
+ <text content="Press y to confirm · ← to go back · Esc to cancel" fg={theme.textFaint} wrapMode="none" />
232
+ </box>
233
+ </Panel>
234
+ )
235
+ }
236
+
237
+ if (dp === "tracking") {
238
+ return (
239
+ <box style={{ flexDirection: "column", alignItems: "center" }}>
240
+ <box style={{ flexDirection: "row" }}>
241
+ <Spinner />
242
+ <text content={` Upgrading to PHP ${target} — ${progress?.status ?? "queued"}…`} fg={theme.textDim} />
243
+ </box>
244
+ <box style={{ height: 1 }} />
245
+ <text content="You can press Esc — it keeps running in the background." fg={theme.textFaint} wrapMode="none" />
246
+ </box>
247
+ )
248
+ }
249
+
250
+ if (dp === "done") {
251
+ return (
252
+ <Panel title=" Done " active>
253
+ <box style={{ flexDirection: "column", width: 56, paddingTop: 1, paddingBottom: 1 }}>
254
+ <box style={{ flexDirection: "row" }}>
255
+ <text content="✓ " fg={theme.good} />
256
+ <text content={truncate(site!.domain, 32)} fg={theme.accent} wrapMode="none" />
257
+ <text content={` is now on PHP ${target}`} fg={theme.text} wrapMode="none" />
258
+ </box>
259
+ <box style={{ height: 1 }} />
260
+ <text content="Esc to close" fg={theme.textFaint} />
261
+ </box>
262
+ </Panel>
263
+ )
264
+ }
265
+
266
+ // error
267
+ return (
268
+ <Panel title=" Upgrade failed " active>
269
+ <box style={{ flexDirection: "column", width: 66, paddingTop: 1, paddingBottom: 1 }}>
270
+ <text content={`✕ ${progress?.error ?? "Something went wrong."}`} fg={theme.bad} />
271
+ <box style={{ height: 1 }} />
272
+ <text content="Press r to choose another version · Esc to close" fg={theme.textFaint} wrapMode="none" />
273
+ </box>
274
+ </Panel>
275
+ )
276
+ }
277
+
278
+ function hints() {
279
+ switch (dp) {
280
+ case "pick":
281
+ return [
282
+ { key: "↑↓/jk", label: "version" },
283
+ { key: "⏎", label: "choose" },
284
+ { key: "esc", label: "cancel" },
285
+ ]
286
+ case "confirm":
287
+ return [
288
+ { key: "y", label: "confirm" },
289
+ { key: "←", label: "back" },
290
+ { key: "esc", label: "cancel" },
291
+ ]
292
+ case "error":
293
+ return [
294
+ { key: "r", label: "retry" },
295
+ { key: "esc", label: "close" },
296
+ ]
297
+ default:
298
+ return [{ key: "esc", label: "close" }]
299
+ }
300
+ }
301
+ }