spinuptui 0.25.0 → 0.26.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 +103 -1
- package/README.md +61 -8
- package/package.json +3 -2
- package/src/index.tsx +134 -11
- package/src/lib/appUpdate.ts +2 -1
- package/src/lib/cliPull.ts +387 -0
- package/src/lib/cliSsh.ts +76 -50
- package/src/lib/cwdDotenv.ts +106 -0
- package/src/lib/dbBackup.ts +9 -5
- package/src/lib/dbSync.ts +15 -1
- package/src/lib/gitDeployKey.ts +0 -0
- package/src/lib/gitStatus.ts +2 -1
- package/src/lib/keychain.ts +3 -1
- package/src/lib/links.ts +23 -0
- package/src/lib/localSetup.ts +164 -0
- package/src/lib/open.ts +10 -8
- package/src/lib/probe.ts +2 -1
- package/src/lib/serverClone.ts +3 -2
- package/src/lib/spawn.ts +17 -0
- package/src/lib/ssh.ts +6 -5
- package/src/lib/sshExec.ts +4 -2
- package/src/lib/wpInventory.ts +2 -1
- package/src/ui/App.tsx +6 -0
- package/src/ui/store.tsx +70 -0
- package/src/ui/views/LocalLink.tsx +62 -11
- package/src/ui/views/LocalSetup.tsx +256 -0
- package/src/ui/views/Search.tsx +38 -66
package/src/ui/views/Search.tsx
CHANGED
|
@@ -9,11 +9,11 @@ import { useKeyboard } from "@opentui/react"
|
|
|
9
9
|
import { theme, statusColor, statusDot } from "../../lib/theme.ts"
|
|
10
10
|
import { classifyStack, stackColor, stackTag } from "../../lib/stack.ts"
|
|
11
11
|
import { truncate } from "../../lib/format.ts"
|
|
12
|
-
import { Panel,
|
|
12
|
+
import { Panel, SiteMetaCell, Spinner } from "../components.tsx"
|
|
13
13
|
import { isDbBackupInFlight } from "../../lib/dbBackup.ts"
|
|
14
14
|
import { isDbSyncInFlight } from "../../lib/dbSync.ts"
|
|
15
15
|
import { List, moveSelection } from "../List.tsx"
|
|
16
|
-
import { ServerDetail, SiteDetail,
|
|
16
|
+
import { ServerDetail, SiteDetail, ControlStrip, CONTROL_STRIP_HEIGHT } from "../Details.tsx"
|
|
17
17
|
import { StatusBar } from "../StatusBar.tsx"
|
|
18
18
|
import { openUrl } from "../../lib/open.ts"
|
|
19
19
|
import { serverWebUrl, siteWebUrl } from "../../lib/spinupweb.ts"
|
|
@@ -35,11 +35,12 @@ function score(haystack: string, q: string): number | null {
|
|
|
35
35
|
|
|
36
36
|
export function Search({ rows }: { rows: number }) {
|
|
37
37
|
const store = useStore()
|
|
38
|
-
const { servers, sites, serverById, setInputMode, setRoute, route, overlayOpen, setHealthServer, setPhpUpgradeSite, setHttpsToggleSite, setPurgeCacheSite, setServerActionsServer, accountSlug, localLinks, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setDbBackupSite, dbBackups, setDbSyncSite, dbSyncs, localSync, setMediaFallbackSite, beginClone, setSudoConnectServer, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setWpInventorySite, setGrantKeySite, runProbe, setEnableLocalSyncSite } = store
|
|
38
|
+
const { servers, sites, serverById, sitesForServer, setInputMode, setRoute, route, overlayOpen, setHealthServer, setPhpUpgradeSite, setHttpsToggleSite, setPurgeCacheSite, setServerActionsServer, accountSlug, localLinks, setLocalLinkSite, openLocalTerminal, openLocalUrl, sshSite, setDnsInventoryServer, setDbBackupSite, dbBackups, setDbSyncSite, dbSyncs, localSync, setMediaFallbackSite, beginClone, setSudoConnectServer, setKumaSite, kumaConfigured, startKumaSetup, startVanityReseed, setWpInventorySite, setGrantKeySite, runProbe, setEnableLocalSyncSite, setVanityServer, vanityJob } = store
|
|
39
39
|
const [query, setQuery] = useState("")
|
|
40
40
|
const [selected, setSelected] = useState(0)
|
|
41
41
|
// "query" = typing/filtering (input focused); "actions" = input blurred so the
|
|
42
|
-
// selected result's single-key actions
|
|
42
|
+
// selected result's single-key actions fire — listed in the Site/Server
|
|
43
|
+
// Control drawer below, same as the Servers tab. Tab/→ enters, ←/Esc exits.
|
|
43
44
|
const [focus, setFocus] = useState<"query" | "actions">("query")
|
|
44
45
|
const [flash, setFlash] = useState<string | null>(null)
|
|
45
46
|
const flashMsg = (m: string) => {
|
|
@@ -160,11 +161,15 @@ export function Search({ rows }: { rows: number }) {
|
|
|
160
161
|
case "L":
|
|
161
162
|
if (current?.kind === "site") setLocalLinkSite(current.site)
|
|
162
163
|
return
|
|
163
|
-
case "K":
|
|
164
|
-
// Grant Spinup's machine key
|
|
165
|
-
//
|
|
166
|
-
|
|
164
|
+
case "K": {
|
|
165
|
+
// Grant Spinup's machine key over SSH — matches Browser's K binding,
|
|
166
|
+
// which is site-anchored but works on a server result too (falls back
|
|
167
|
+
// to the server's first key-eligible site, mirroring Browser.tsx).
|
|
168
|
+
const anchor =
|
|
169
|
+
current?.kind === "site" ? current.site : current?.kind === "server" ? sitesForServer(current.server.id).find((s) => s.site_user) : undefined
|
|
170
|
+
if (anchor) setGrantKeySite(anchor)
|
|
167
171
|
return
|
|
172
|
+
}
|
|
168
173
|
case "s":
|
|
169
174
|
if (current?.kind === "site") flashMsg(sshSite(current.site.id))
|
|
170
175
|
return
|
|
@@ -262,6 +267,17 @@ export function Search({ rows }: { rows: number }) {
|
|
|
262
267
|
case "N":
|
|
263
268
|
if (current?.kind === "server") setDnsInventoryServer(current.server)
|
|
264
269
|
return
|
|
270
|
+
case "V": {
|
|
271
|
+
// Create the vanity site at the server's own hostname, or resume an
|
|
272
|
+
// unfinished build — same gating as Browser's V.
|
|
273
|
+
if (current?.kind === "server") {
|
|
274
|
+
const s = current.server
|
|
275
|
+
const resumable = vanityJob && vanityJob.step !== "done" && vanityJob.serverId === s.id
|
|
276
|
+
const hasVanity = sitesForServer(s.id).some((site) => site.domain.toLowerCase() === s.name.toLowerCase())
|
|
277
|
+
if (resumable || !hasVanity) setVanityServer(s)
|
|
278
|
+
}
|
|
279
|
+
return
|
|
280
|
+
}
|
|
265
281
|
}
|
|
266
282
|
})
|
|
267
283
|
|
|
@@ -273,22 +289,14 @@ export function Search({ rows }: { rows: number }) {
|
|
|
273
289
|
{ key: "Tab/→", label: "actions" },
|
|
274
290
|
{ key: "esc", label: "dashboard" },
|
|
275
291
|
]
|
|
276
|
-
:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{ key: "←/esc", label: "back" },
|
|
283
|
-
]
|
|
284
|
-
: [
|
|
285
|
-
// The Actions panel (Details pane) already lists every per-site key —
|
|
286
|
-
// this footer is nav-only now, so the two can't drift out of sync.
|
|
287
|
-
{ key: "↑↓", label: "select" },
|
|
288
|
-
{ key: "←/esc", label: "back" },
|
|
289
|
-
]
|
|
292
|
+
: [
|
|
293
|
+
// The Control drawer below already lists every key for the selected
|
|
294
|
+
// result — this footer stays nav-only so the two can't drift out of sync.
|
|
295
|
+
{ key: "↑↓", label: "select" },
|
|
296
|
+
{ key: "←/esc", label: "back" },
|
|
297
|
+
]
|
|
290
298
|
|
|
291
|
-
const listRows = Math.max(3, rows -
|
|
299
|
+
const listRows = Math.max(3, rows - 9 - CONTROL_STRIP_HEIGHT) // input box (3) + status bar (1) + chrome + control drawer
|
|
292
300
|
|
|
293
301
|
return (
|
|
294
302
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
@@ -374,14 +382,9 @@ export function Search({ rows }: { rows: number }) {
|
|
|
374
382
|
/>
|
|
375
383
|
</Panel>
|
|
376
384
|
|
|
377
|
-
<Panel title=
|
|
385
|
+
<Panel title=" Details " width={64}>
|
|
378
386
|
{!current ? (
|
|
379
387
|
<text content="No selection" fg={theme.textFaint} />
|
|
380
|
-
) : focus === "actions" ? (
|
|
381
|
-
<ActionsCard
|
|
382
|
-
result={current}
|
|
383
|
-
serverName={current.kind === "site" ? (serverById(current.site.server_id)?.name ?? "—") : current.server.name}
|
|
384
|
-
/>
|
|
385
388
|
) : current.kind === "server" ? (
|
|
386
389
|
<ServerDetail server={current.server} siteCount={store.sitesForServer(current.server.id).length} />
|
|
387
390
|
) : (
|
|
@@ -390,44 +393,13 @@ export function Search({ rows }: { rows: number }) {
|
|
|
390
393
|
</Panel>
|
|
391
394
|
</box>
|
|
392
395
|
|
|
393
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
396
|
+
<ControlStrip
|
|
397
|
+
site={current?.kind === "site" ? current.site : null}
|
|
398
|
+
server={current?.kind === "server" ? current.server : null}
|
|
399
|
+
serverName={current?.kind === "site" ? (serverById(current.site.server_id)?.name ?? "—") : current?.kind === "server" ? current.server.name : "—"}
|
|
400
|
+
/>
|
|
397
401
|
|
|
398
|
-
|
|
399
|
-
const isSite = result.kind === "site"
|
|
400
|
-
const name = isSite ? result.site.domain : result.server.name
|
|
401
|
-
const status = isSite ? result.site.status : result.server.connection_status
|
|
402
|
-
// Server results reuse the SAME Server Control suite as the Browser detail pane.
|
|
403
|
-
const groups = isSite ? siteGroups(result.site.is_wordpress, isVanityPair(result.site.domain, serverName)) : SERVER_CONTROL
|
|
404
|
-
return (
|
|
405
|
-
<box style={{ flexDirection: "column" }}>
|
|
406
|
-
<box style={{ flexDirection: "row" }}>
|
|
407
|
-
<text content={truncate(name, 30)} fg={theme.text} attributes={1} wrapMode="none" />
|
|
408
|
-
<box style={{ flexGrow: 1 }} />
|
|
409
|
-
<StatusBadge status={status} />
|
|
410
|
-
</box>
|
|
411
|
-
{isSite ? (
|
|
412
|
-
<>
|
|
413
|
-
<text content={(result.site.https?.enabled ? "https://" : "http://") + result.site.domain} fg={theme.accent} wrapMode="none" />
|
|
414
|
-
<box style={{ height: 1 }} />
|
|
415
|
-
<Field label="Server" value={truncate(serverName, 28)} labelWidth={8} />
|
|
416
|
-
<Field label="PHP" value={result.site.php_version ?? "—"} labelWidth={8} />
|
|
417
|
-
<Field label="Stack" value={classifyStack(result.site)} valueColor={stackColor(classifyStack(result.site))} labelWidth={8} />
|
|
418
|
-
</>
|
|
419
|
-
) : (
|
|
420
|
-
<>
|
|
421
|
-
<text content={result.server.ip_address ?? "—"} fg={theme.accent} wrapMode="none" />
|
|
422
|
-
<box style={{ height: 1 }} />
|
|
423
|
-
<Field label="Provider" value={result.server.provider_name ?? "—"} labelWidth={9} />
|
|
424
|
-
<Field label="Region" value={result.server.region ?? "—"} labelWidth={9} />
|
|
425
|
-
</>
|
|
426
|
-
)}
|
|
427
|
-
<box style={{ height: 1 }} />
|
|
428
|
-
<ControlPanel heading={isSite ? "Site Control" : "Server Control"} groups={groups} />
|
|
429
|
-
<box style={{ height: 1 }} />
|
|
430
|
-
<text content="↑↓ select · ←/Esc back to search" fg={theme.textFaint} wrapMode="none" />
|
|
402
|
+
<StatusBar hints={hints} message={flash ?? undefined} showGlobal={false} />
|
|
431
403
|
</box>
|
|
432
404
|
)
|
|
433
405
|
}
|