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,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
+ }