spinuptui 0.21.1 → 0.22.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 +96 -1
- package/README.md +26 -10
- package/package.json +1 -1
- package/src/index.tsx +20 -0
- package/src/lib/cliSsh.ts +65 -19
- package/src/lib/dbSync.ts +12 -2
- package/src/lib/probe.ts +13 -3
- package/src/lib/serverClone.ts +127 -17
- package/src/lib/sshExec.ts +93 -0
- package/src/lib/sshExecLog.ts +31 -0
- package/src/lib/sshExecPolicy.ts +51 -0
- package/src/lib/stack.ts +15 -9
- package/src/ui/Details.tsx +151 -53
- package/src/ui/Onboarding.tsx +1 -1
- package/src/ui/components.tsx +84 -14
- package/src/ui/store.tsx +11 -2
- package/src/ui/views/Browser.tsx +35 -43
- package/src/ui/views/CloneWizard.tsx +9 -12
- package/src/ui/views/Search.tsx +16 -49
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,99 @@ versions; such changes are called out here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.22.0] - 2026-07-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`spinuptui ssh-exec <domain> -- <command>`** — resolves the domain's SSH
|
|
18
|
+
target (same as `spinuptui ssh`) and runs `<command>` on it, but only if
|
|
19
|
+
it's read-only: anything matching a write/restart/destructive pattern
|
|
20
|
+
(plugin/theme changes, DB writes, service restarts, redirecting output to
|
|
21
|
+
a real file, package installs, etc.) is denied before it ever reaches the
|
|
22
|
+
server. Built for external tooling that needs to run diagnostic commands
|
|
23
|
+
unattended — any agent configured to touch a server only through this
|
|
24
|
+
command inherits the read-only guarantee without building its own guard.
|
|
25
|
+
Prints structured JSON (`stdout`/`stderr`/`exitCode` on success,
|
|
26
|
+
`reason:"command_denied"` on refusal) and logs every attempt — allowed,
|
|
27
|
+
denied, or unresolved — to `<config dir>/logs/ssh-exec-audit.jsonl`.
|
|
28
|
+
- **Refreshing a vanity page's HTML is now discoverable from where you'd look
|
|
29
|
+
for it.** A shared, grouped, key-chipped action list (`siteGroups()`) now
|
|
30
|
+
shows every per-site action — including `R` (refresh vanity page) for the
|
|
31
|
+
one site per server whose domain is the server's own hostname — in Search's
|
|
32
|
+
Actions pane and, on the Servers tab, in the bottom **Control strip**
|
|
33
|
+
(below). Previously `R` only lived inside the `m` monitoring overlay.
|
|
34
|
+
- **The Servers tab's bottom "Local" drawer is now a full-width Control
|
|
35
|
+
strip.** With a site selected it shows Site Control (Open/Remote/Local/
|
|
36
|
+
Vanity/Server); with a server selected (no site drilled into) it now shows
|
|
37
|
+
real **Server Control** (Clone/Access/Manage/Open) instead of a generic
|
|
38
|
+
placeholder. Each group's items flow and wrap across the full terminal
|
|
39
|
+
width instead of stacking one per line, so even a long group like Remote
|
|
40
|
+
fits in 1 line — each group's label sits in its own left column (pinned to
|
|
41
|
+
the top if its items wrap), and item labels are terser now that they don't
|
|
42
|
+
need to repeat their group's name (e.g. "In browser" under OPEN, not "Open
|
|
43
|
+
site in browser"). The Sites list also dropped its per-row status badges
|
|
44
|
+
(linked/keys/HTTPS/cache/backup/updates) — that info is already shown in
|
|
45
|
+
the Details pane for the selected site — to give the domain name more room;
|
|
46
|
+
Servers/Sites/Details are back to a simple 3-pane row.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- **Servers and Search footers no longer duplicate the action list.** Both
|
|
50
|
+
views' key hints for a selected site are now nav-only (`select` / `back`);
|
|
51
|
+
the full, always-in-sync action list lives in the Control strip (or
|
|
52
|
+
Search's Actions pane). Also fixed a stale Search hint (`⏎ open site`) that was
|
|
53
|
+
shown even for a selected server row, where Enter is a no-op.
|
|
54
|
+
|
|
55
|
+
## [0.21.2] - 2026-07-10
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- **Onboarding's API token link was a 404.** `https://spinupwp.app/account/api/`
|
|
59
|
+
no longer resolves, and the correct URL is account-specific (a company slug
|
|
60
|
+
we can't know before the user has a token) — replaced with generic
|
|
61
|
+
"Settings → API Tokens" navigation instructions in onboarding and the README.
|
|
62
|
+
- **Bedrock detection now works at any nesting depth, everywhere it matters.**
|
|
63
|
+
A Bedrock install whose project root isn't directly under the site's files
|
|
64
|
+
root (e.g. moved into its own subdirectory) could go undetected — the
|
|
65
|
+
plugins/themes view (`p`) reported "no WordPress core found" even though the
|
|
66
|
+
site worked fine and SpinupWP itself knew it was WordPress. Detection now
|
|
67
|
+
anchors on the site's configured Public Folder (falling back to a bounded
|
|
68
|
+
search only when that's unset) instead of a fixed, shallow search depth:
|
|
69
|
+
- `p` (plugins/themes) finds core correctly.
|
|
70
|
+
- `d` (identify app) correctly labels it "Bedrock" again, not just generic
|
|
71
|
+
WordPress.
|
|
72
|
+
- The clone wizard's Bedrock pull no longer hardcodes `web/`. It detects the
|
|
73
|
+
real project root independently on both source and destination — they're
|
|
74
|
+
not assumed to match, since the destination's layout comes from the git
|
|
75
|
+
repo's own committed content, not the source's on-disk state. A cheap
|
|
76
|
+
pre-flight check also catches a source whose files don't match its own
|
|
77
|
+
configured Public Folder *before* a destination site gets created for a
|
|
78
|
+
clone that can't succeed; the pull chain's own detection step is the
|
|
79
|
+
real, authoritative check either way.
|
|
80
|
+
- The Stacks tab's default (pre-probe) "Bedrock" label also recognizes a
|
|
81
|
+
webroot like `site/web`, not just an exact `web`.
|
|
82
|
+
- **A first-time local Bedrock pull now explains itself.** "Pull production →
|
|
83
|
+
local" (`p` in Search) backs up the local database before overwriting it —
|
|
84
|
+
on a fresh Bedrock checkout with no `vendor/` yet, that step failed with
|
|
85
|
+
wp-cli's generic "not a WordPress install" error. The message now adds
|
|
86
|
+
"Run `composer install` in your local checkout first" when that's the
|
|
87
|
+
likely cause.
|
|
88
|
+
- **Clone-wizard error messages no longer get cut off at a fixed, often-too-
|
|
89
|
+
short length.** The site roster, Git-access deploy-key list, and DNS
|
|
90
|
+
cutover roster all truncated error text to a small fixed character count
|
|
91
|
+
regardless of how much space was actually available. Errors now fill
|
|
92
|
+
whatever room the row has, so a full-sentence error like the Bedrock
|
|
93
|
+
layout mismatch above is far more likely to be readable inline instead of
|
|
94
|
+
needing a drill-down for basic context.
|
|
95
|
+
|
|
96
|
+
### Notes
|
|
97
|
+
- Nested Bedrock layouts (where the project root isn't directly under the
|
|
98
|
+
site's files root) are now correctly detected in the plugins/themes view,
|
|
99
|
+
app identification, and the clone wizard's size estimate — all live-
|
|
100
|
+
tested. A standard (non-nested) Bedrock clone through the rewritten pull
|
|
101
|
+
chain was also live-tested end to end, including a real DNS cutover. The
|
|
102
|
+
clone wizard's pull for a *nested* Bedrock site detects a source/repo
|
|
103
|
+
mismatch and refuses cleanly before creating anything, but the success
|
|
104
|
+
path — a genuinely nested repo cloning through to completion — hasn't
|
|
105
|
+
been live-validated yet. If you hit this, we'd welcome a report.
|
|
106
|
+
|
|
14
107
|
## [0.21.1] - 2026-07-10
|
|
15
108
|
|
|
16
109
|
### Fixed
|
|
@@ -1073,7 +1166,9 @@ Initial tagged release.
|
|
|
1073
1166
|
### Notes
|
|
1074
1167
|
- Read-only release: works with a SpinupWP **Read Only** API token.
|
|
1075
1168
|
|
|
1076
|
-
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.
|
|
1169
|
+
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.22.0...HEAD
|
|
1170
|
+
[0.22.0]: https://github.com/mwender/spinupwp-tui/compare/v0.21.2...v0.22.0
|
|
1171
|
+
[0.21.2]: https://github.com/mwender/spinupwp-tui/compare/v0.21.1...v0.21.2
|
|
1077
1172
|
[0.21.1]: https://github.com/mwender/spinupwp-tui/compare/v0.21.0...v0.21.1
|
|
1078
1173
|
[0.21.0]: https://github.com/mwender/spinupwp-tui/compare/v0.20.1...v0.21.0
|
|
1079
1174
|
[0.20.1]: https://github.com/mwender/spinupwp-tui/compare/v0.20.0...v0.20.1
|
package/README.md
CHANGED
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
|
|
30
30
|
- **Fleet dashboard** — connection status, disk usage, pending reboots/upgrades,
|
|
31
31
|
WP update counts, EOL Ubuntu flags, and a recent-activity feed for every server.
|
|
32
|
-
- **Server & site browser** — a three-pane navigator
|
|
33
|
-
|
|
32
|
+
- **Server & site browser** — a three-pane navigator (servers → sites →
|
|
33
|
+
details) with a full-width Control strip below showing every action for
|
|
34
|
+
whatever's selected — Site Control or Server Control, grouped and always
|
|
35
|
+
visible, no hidden key combos.
|
|
34
36
|
- **Stack detection & fleet composition** — classifies every site as Standard WP,
|
|
35
37
|
Bedrock, or Non-WP, with a PHP-version breakdown and an on-demand SSH probe for
|
|
36
38
|
precise identification. → [docs/stack-detection.md](docs/stack-detection.md)
|
|
@@ -74,10 +76,13 @@
|
|
|
74
76
|
[docs/uptime-kuma.md](docs/uptime-kuma.md)
|
|
75
77
|
- **Non-interactive CLI subcommands for external tooling** — `spinuptui ssh <domain>`
|
|
76
78
|
resolves a domain to a live SSH target with a live connectivity probe;
|
|
77
|
-
`spinuptui
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
`spinuptui ssh-exec <domain> -- <command>` runs a command over SSH, but only
|
|
80
|
+
if it's read-only — anything that looks like a write/restart/destructive
|
|
81
|
+
action is denied rather than run, so any agent using it inherits that
|
|
82
|
+
guarantee without building its own guard; `spinuptui incidents <domain>` /
|
|
83
|
+
`--all` surfaces Uptime Kuma's down/up history as JSON. Built so another
|
|
84
|
+
agent/script can go from just a domain to "what's wrong and how do I get
|
|
85
|
+
in" with no manual lookup. → [CLI subcommands](#cli-subcommands)
|
|
81
86
|
- **Completion toasts** — a non-focus-stealing toast when a background write
|
|
82
87
|
(PHP upgrade, reboot, DNS resolve, …) finishes.
|
|
83
88
|
- **Release notes** — an in-app "what's new" after every update, sourced
|
|
@@ -96,10 +101,9 @@
|
|
|
96
101
|
```sh
|
|
97
102
|
curl -fsSL https://bun.sh/install | bash
|
|
98
103
|
```
|
|
99
|
-
- A SpinupWP API token — create one
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
PHP version.
|
|
104
|
+
- A SpinupWP API token — create one in your SpinupWP dashboard under
|
|
105
|
+
**Settings → API Tokens**. **Read Only** scope is enough to browse; use
|
|
106
|
+
**Read/Write** if you want to upgrade a site's PHP version.
|
|
103
107
|
|
|
104
108
|
## Install & run
|
|
105
109
|
|
|
@@ -167,6 +171,17 @@ spinuptui ssh <domain> Print SSH access info for a site as JSON (resolves the
|
|
|
167
171
|
site's server, builds its SSH target, and runs a live
|
|
168
172
|
connectivity probe — for external tooling, e.g. an
|
|
169
173
|
incident-diagnostics agent handed only a domain)
|
|
174
|
+
spinuptui ssh-exec <domain> -- <command> Resolve the domain's SSH target and
|
|
175
|
+
run <command> on it, but only if the command is
|
|
176
|
+
read-only — anything matching a write/restart/destructive
|
|
177
|
+
pattern (plugin/theme changes, DB writes, service
|
|
178
|
+
restarts, file redirection to a real path, etc.) is
|
|
179
|
+
denied instead of run. Prints JSON: on success,
|
|
180
|
+
{ok:true, exitCode, stdout, stderr, ...}; on denial,
|
|
181
|
+
{ok:false, reason:"command_denied", message}. Every
|
|
182
|
+
attempt (allowed, denied, or unresolved) is appended to
|
|
183
|
+
<config dir>/logs/ssh-exec-audit.jsonl. Quote <command>
|
|
184
|
+
as one shell argument if it needs pipes or redirects.
|
|
170
185
|
spinuptui incidents <domain> | --all [--hours N] Print Uptime Kuma down/up
|
|
171
186
|
incidents as JSON, scoped to sites SpinupTUI manages
|
|
172
187
|
monitoring for (config.json's kumaMonitors) — --all
|
|
@@ -249,6 +264,7 @@ These can be set in `config.json` or via an environment variable:
|
|
|
249
264
|
| `H` | Enable / disable HTTPS on a site (Servers / Stacks / Search; needs a Read/Write token) |
|
|
250
265
|
| `P` | Purge a site's page cache + object cache (Servers / Stacks / Search; needs a Read/Write token) |
|
|
251
266
|
| `m` / `M` | Site/server monitoring — a two-pane browser of this site's Uptime Kuma monitors (`M` is an alias, since lowercase `m` is taken in Search/Stacks). Inside: `↑`/`↓` selects a monitor, `a` registers/recalibrates/repairs it (Front page and Cache bypass open a check-window picker), `x` removes Front page or Cache bypass, `o` opens the selected monitor in Kuma, `d` runs the site doctor, `n` shows/edits alert wiring, and vanity sites add `R`/`r` for page refresh/secret rotation (Servers / Stacks / Search) |
|
|
267
|
+
| `R` | Refresh a vanity page's HTML to the currently bundled version — no need to open `m` first. Shown under Vanity in the Servers tab's Control strip and Search's Actions list, for the one site per server whose domain is the server's own hostname |
|
|
252
268
|
| `a` | Server actions: reboot / restart a service (Servers / Search; needs a Read/Write token) |
|
|
253
269
|
| `c` | Create a new server (Servers tab; needs a Read/Write token) |
|
|
254
270
|
| `V` | Add a vanity site at the server's own hostname — DNS + site + HTTPS + SSH-key handoff (Servers tab; offered when no hostname site exists; needs a Read/Write token) |
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { isDevMode } from "./dev/devMode.ts"
|
|
|
15
15
|
import { SpinupWPClient } from "./api/client.ts"
|
|
16
16
|
import { resolveSshAccess } from "./lib/cliSsh.ts"
|
|
17
17
|
import { resolveIncidents } from "./lib/cliIncidents.ts"
|
|
18
|
+
import { execSshCommand } from "./lib/sshExec.ts"
|
|
18
19
|
|
|
19
20
|
const args = process.argv.slice(2)
|
|
20
21
|
const positionals = args.filter((a) => !a.startsWith("-"))
|
|
@@ -29,6 +30,8 @@ Usage:
|
|
|
29
30
|
spinuptui login Set or update your saved API token
|
|
30
31
|
spinuptui where Print the config file path and token source
|
|
31
32
|
spinuptui ssh <domain> Print SSH access info for a site (JSON)
|
|
33
|
+
spinuptui ssh-exec <domain> -- <command> Run a read-only command over SSH
|
|
34
|
+
(JSON); denies anything that looks like a remote write
|
|
32
35
|
spinuptui incidents <domain> | --all [--hours N] Print Uptime Kuma
|
|
33
36
|
down/up incidents for a site or the whole fleet (JSON)
|
|
34
37
|
spinuptui --version Print the version
|
|
@@ -68,6 +71,23 @@ if (command === "ssh") {
|
|
|
68
71
|
process.exit(result.ok ? 0 : 1)
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
if (command === "ssh-exec") {
|
|
75
|
+
const dashIdx = args.indexOf("--")
|
|
76
|
+
const domain = positionals[1]
|
|
77
|
+
const remoteCmd = dashIdx !== -1 ? args.slice(dashIdx + 1).join(" ") : ""
|
|
78
|
+
if (!domain || dashIdx === -1 || !remoteCmd.trim()) {
|
|
79
|
+
console.error(
|
|
80
|
+
JSON.stringify({ ok: false, reason: "usage", message: "Usage: spinuptui ssh-exec <domain> -- <command>" }),
|
|
81
|
+
)
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
const cfg = loadConfig()
|
|
85
|
+
const client = new SpinupWPClient(cfg)
|
|
86
|
+
const result = await execSshCommand(domain, remoteCmd, client, cfg)
|
|
87
|
+
console.log(JSON.stringify(result))
|
|
88
|
+
process.exit(result.ok ? 0 : 1)
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
if (command === "incidents") {
|
|
72
92
|
const rest = args.slice(1)
|
|
73
93
|
const hoursIdx = rest.indexOf("--hours")
|
package/src/lib/cliSsh.ts
CHANGED
|
@@ -40,13 +40,32 @@ export type SshAccessResult =
|
|
|
40
40
|
const GRANT_KEY_REMEDY =
|
|
41
41
|
"Run `spinuptui`, select this site in the Browser view, and press K to grant this device's key (sudo must be connected on the server first — press S)."
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// Resolved SSH connection details for a domain, without having tested them.
|
|
44
|
+
export interface SshTargetInfo {
|
|
45
|
+
domain: string
|
|
46
|
+
primaryDomain: string
|
|
47
|
+
sshTarget: string
|
|
48
|
+
port: number | null
|
|
49
|
+
server: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type SshTargetResolution =
|
|
53
|
+
| { ok: true; info: SshTargetInfo }
|
|
54
|
+
| { ok: false; result: SshAccessResult & { ok: false } }
|
|
55
|
+
|
|
56
|
+
// Domain -> site -> server -> SSH target/port, with no live connectivity check.
|
|
57
|
+
// Split out of resolveSshAccess so callers that are about to run a real command
|
|
58
|
+
// (which is itself a connectivity test) don't pay for a redundant probe first.
|
|
59
|
+
export async function resolveSshTargetInfo(
|
|
44
60
|
domain: string,
|
|
45
61
|
client: SpinupWPClientLike,
|
|
46
62
|
cfg: AppConfig,
|
|
47
|
-
): Promise<
|
|
63
|
+
): Promise<SshTargetResolution> {
|
|
48
64
|
if (!cfg.token) {
|
|
49
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
result: { ok: false, domain, reason: "no_token", message: "No API token configured. Run `spinuptui login`." },
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
let site
|
|
@@ -59,9 +78,12 @@ export async function resolveSshAccess(
|
|
|
59
78
|
if (matches.length === 0) {
|
|
60
79
|
return {
|
|
61
80
|
ok: false,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
result: {
|
|
82
|
+
ok: false,
|
|
83
|
+
domain,
|
|
84
|
+
reason: "site_not_found",
|
|
85
|
+
message: `No site matching "${domain}" found in this SpinupWP account.`,
|
|
86
|
+
},
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
if (matches.length > 1) {
|
|
@@ -73,10 +95,13 @@ export async function resolveSshAccess(
|
|
|
73
95
|
)
|
|
74
96
|
return {
|
|
75
97
|
ok: false,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
result: {
|
|
99
|
+
ok: false,
|
|
100
|
+
domain,
|
|
101
|
+
reason: "multiple_matches",
|
|
102
|
+
message: `"${domain}" matches ${matches.length} sites in this account — cannot pick one automatically.`,
|
|
103
|
+
candidates,
|
|
104
|
+
},
|
|
80
105
|
}
|
|
81
106
|
}
|
|
82
107
|
site = matches[0]
|
|
@@ -85,27 +110,48 @@ export async function resolveSshAccess(
|
|
|
85
110
|
if (err instanceof ApiError && err.status === 401) {
|
|
86
111
|
return {
|
|
87
112
|
ok: false,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
result: {
|
|
114
|
+
ok: false,
|
|
115
|
+
domain,
|
|
116
|
+
reason: "token_rejected",
|
|
117
|
+
message: "Saved API token was rejected (401) — it may be expired or revoked.",
|
|
118
|
+
remedy: "Run `spinuptui login` to save a fresh token.",
|
|
119
|
+
},
|
|
92
120
|
}
|
|
93
121
|
}
|
|
94
122
|
const message = err instanceof ApiError ? err.message : `Unexpected error: ${(err as Error).message}`
|
|
95
|
-
return { ok: false, domain, reason: "api_error", message }
|
|
123
|
+
return { ok: false, result: { ok: false, domain, reason: "api_error", message } }
|
|
96
124
|
}
|
|
97
125
|
|
|
98
126
|
if (!server.ip_address) {
|
|
99
127
|
return {
|
|
100
128
|
ok: false,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
129
|
+
result: {
|
|
130
|
+
ok: false,
|
|
131
|
+
domain,
|
|
132
|
+
reason: "server_has_no_ip",
|
|
133
|
+
message: `Server "${server.name}" has no IP address on file.`,
|
|
134
|
+
},
|
|
104
135
|
}
|
|
105
136
|
}
|
|
106
137
|
|
|
107
138
|
const target = resolveSiteSshTarget(site, server, cfg.sshUser)
|
|
108
139
|
const port = server.ssh_port && server.ssh_port !== 22 ? server.ssh_port : null
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
info: { domain, primaryDomain: site.domain, sshTarget: target, port, server: server.name },
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function resolveSshAccess(
|
|
148
|
+
domain: string,
|
|
149
|
+
client: SpinupWPClientLike,
|
|
150
|
+
cfg: AppConfig,
|
|
151
|
+
): Promise<SshAccessResult> {
|
|
152
|
+
const resolution = await resolveSshTargetInfo(domain, client, cfg)
|
|
153
|
+
if (!resolution.ok) return resolution.result
|
|
154
|
+
const { primaryDomain, sshTarget: target, port, server: serverName } = resolution.info
|
|
109
155
|
const portOpt = port ? ["-p", String(port)] : []
|
|
110
156
|
|
|
111
157
|
let proc: ReturnType<typeof Bun.spawn>
|
|
@@ -132,7 +178,7 @@ export async function resolveSshAccess(
|
|
|
132
178
|
const stderr = await new Response(proc.stderr as ReadableStream<Uint8Array>).text()
|
|
133
179
|
|
|
134
180
|
if (exitCode === 0) {
|
|
135
|
-
return { ok: true, domain, primaryDomain
|
|
181
|
+
return { ok: true, domain, primaryDomain, sshTarget: target, port, server: serverName }
|
|
136
182
|
}
|
|
137
183
|
|
|
138
184
|
if (/permission denied/i.test(stderr)) {
|
package/src/lib/dbSync.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs"
|
|
16
16
|
import { join, dirname } from "node:path"
|
|
17
17
|
import type { Server, Site } from "../api/types.ts"
|
|
18
|
-
import { expandPath, findProjectRoot, type LocalLink } from "./local.ts"
|
|
18
|
+
import { expandPath, findProjectRoot, resolveLocalLink, type LocalKind, type LocalLink } from "./local.ts"
|
|
19
19
|
import { remoteDocRoot, backupBaseName, SSH_OPTS, sshPort, scpPort, runProcess, meaningfulError } from "./dbBackup.ts"
|
|
20
20
|
|
|
21
21
|
// Read a KEY=value from a dotenv file, tolerating surrounding quotes. null if absent.
|
|
@@ -99,6 +99,7 @@ export interface DbSyncPlan {
|
|
|
99
99
|
localHost: string // bare local host (hook env)
|
|
100
100
|
hookPath: string | null // bin/sync.d/post-import.sh, if present
|
|
101
101
|
prefixWarning: string | null // set when remote/local table prefixes differ
|
|
102
|
+
localKind: LocalKind // for a tailored hint if local wp-cli can't find an install (e.g. fresh Bedrock checkout)
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export type SyncPlanResult = { ok: true; plan: DbSyncPlan } | { ok: false; error: string }
|
|
@@ -161,6 +162,7 @@ export function planDbSync(
|
|
|
161
162
|
? join(localRoot, "bin", "sync.d", "post-import.sh")
|
|
162
163
|
: null,
|
|
163
164
|
prefixWarning,
|
|
165
|
+
localKind: resolveLocalLink(link).kind,
|
|
164
166
|
},
|
|
165
167
|
}
|
|
166
168
|
}
|
|
@@ -209,7 +211,15 @@ export async function runDbSync(plan: DbSyncPlan, domain: string, onProgress: (p
|
|
|
209
211
|
}
|
|
210
212
|
const localSql = plan.localBackupPath.replace(/\.gz$/, "")
|
|
211
213
|
const lb = await wp(`wp db export "${localSql}" >/dev/null && gzip -f "${localSql}"`, 300_000)
|
|
212
|
-
if (lb.code !== 0)
|
|
214
|
+
if (lb.code !== 0) {
|
|
215
|
+
// wp-cli's own "not a WordPress install" error is a common first-pull snag on a
|
|
216
|
+
// fresh checkout — a Bedrock project needs `composer install` to build vendor/ +
|
|
217
|
+
// web/wp before wp-cli (or wp-cli.yml) can find anything there at all. Appended
|
|
218
|
+
// after meaningfulError's own pick (not passed as its fallback), since that's
|
|
219
|
+
// only used when stderr has NO meaningful line — not the case here.
|
|
220
|
+
const hint = plan.localKind === "bedrock" && /does not seem to be a WordPress install/i.test(lb.stderr) ? " Run `composer install` in your local checkout first." : ""
|
|
221
|
+
return fail(`Local DB backup failed — ${meaningfulError(lb.stderr, "couldn't export the local database.")}${hint}`, "local-backup")
|
|
222
|
+
}
|
|
213
223
|
|
|
214
224
|
// 2) Export prod + download the gzip (same remote pipeline as the backup).
|
|
215
225
|
onProgress({ stage: "export", domain })
|
package/src/lib/probe.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// view's SSH usage.
|
|
18
18
|
|
|
19
19
|
import type { Server, Site } from "../api/types.ts"
|
|
20
|
+
import { detectWpDirScript } from "./serverClone.ts"
|
|
20
21
|
import { theme } from "./theme.ts"
|
|
21
22
|
|
|
22
23
|
export type ProbeKind = "wordpress" | "bedrock" | "whmcs" | "laravel" | "static" | "unknown"
|
|
@@ -76,12 +77,21 @@ function safePublicFolder(pf: string | null): string {
|
|
|
76
77
|
return pf
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function buildRemoteScript(publicFolder: string | null): string {
|
|
80
|
+
function buildRemoteScript(domain: string, publicFolder: string | null): string {
|
|
80
81
|
const pf = safePublicFolder(publicFolder)
|
|
82
|
+
const root = `/sites/${domain}/files`
|
|
81
83
|
return [
|
|
84
|
+
// detectWpDirScript anchors on the configured public folder (falling back to a
|
|
85
|
+
// bounded find), so Bedrock's project root — one level above wherever its core
|
|
86
|
+
// actually lives — is found at any nesting depth, not just when composer.json
|
|
87
|
+
// sits directly in the files root. $D/$W from this are shadowed below by the
|
|
88
|
+
// probe's own "webroot per public_folder" variables, which the rest of this
|
|
89
|
+
// script (WHMCS/index/WP-version checks) intentionally keeps using as-is.
|
|
90
|
+
detectWpDirScript(root, publicFolder ?? undefined),
|
|
91
|
+
`BEDROCKROOT="$B"`,
|
|
82
92
|
'F="$HOME/files"',
|
|
83
93
|
`W="$HOME/files${pf}"`,
|
|
84
|
-
'echo ===BEDROCK;
|
|
94
|
+
'echo ===BEDROCK; [ -n "$BEDROCKROOT" ] && echo yes || echo no',
|
|
85
95
|
'echo ===APPLICATION; test -f "$F/config/application.php" && echo yes || echo no',
|
|
86
96
|
'echo ===ARTISAN; test -f "$F/artisan" && echo yes || echo no',
|
|
87
97
|
'echo ===WHMCSCONF; test -f "$W/configuration.php" && echo yes || echo no',
|
|
@@ -103,7 +113,7 @@ export async function probeSite(
|
|
|
103
113
|
|
|
104
114
|
let proc: ReturnType<typeof Bun.spawn>
|
|
105
115
|
try {
|
|
106
|
-
proc = Bun.spawn(["ssh", ...SSH_OPTS, target, buildRemoteScript(site.public_folder)], {
|
|
116
|
+
proc = Bun.spawn(["ssh", ...SSH_OPTS, target, buildRemoteScript(site.domain, site.public_folder)], {
|
|
107
117
|
stdout: "pipe",
|
|
108
118
|
stderr: "pipe",
|
|
109
119
|
stdin: "ignore",
|