spinuptui 0.19.1 → 0.20.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 +70 -1
- package/README.md +124 -578
- package/package.json +1 -1
- package/src/dev/fixtures.ts +494 -5
- package/src/lib/serverClone.ts +26 -4
- package/src/lib/stack.ts +23 -0
- package/src/lib/wpCli.ts +30 -0
- package/src/lib/wpInventory.ts +7 -3
- package/src/ui/store.tsx +8 -11
- package/src/ui/views/CloneWizard.tsx +3 -1
package/package.json
CHANGED
package/src/dev/fixtures.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
// Example fleet for Dev Mode (SPINUP_DEV_MODE=1) — a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Example fleet for Dev Mode (SPINUP_DEV_MODE=1) — a deliberately varied fleet (12
|
|
2
|
+
// servers / ~80 sites) so every view has plenty to show: EOL Ubuntu and EOL PHP
|
|
3
|
+
// servers/sites, Bedrock sites, non-WP apps, an empty server, a mid-provisioning
|
|
4
|
+
// server, a self-hosted/"custom" server, disabled-HTTPS sites, stale/missing
|
|
5
|
+
// backups, and a spread of disk usage / pending-update counts. All domains are
|
|
5
6
|
// example.com/.org/.net — never a real client's.
|
|
6
7
|
//
|
|
8
|
+
// Sites 1001-1006 are hand-authored (kept stable so anything that references
|
|
9
|
+
// them by id stays meaningful); sites 1007+ are generated deterministically
|
|
10
|
+
// (see GENERATED_SITE_PLAN below) — no Math.random/Date.now, so the fleet is
|
|
11
|
+
// identical across runs (reproducible screenshots, stable diffs).
|
|
12
|
+
//
|
|
7
13
|
// freshFixtures() returns a deep clone so MockSpinupWPClient can mutate its own copy
|
|
8
14
|
// freely without leaking state across StoreProvider remounts.
|
|
9
15
|
|
|
@@ -84,9 +90,153 @@ const SERVERS: Server[] = [
|
|
|
84
90
|
created_at: "2025-01-08T16:45:00Z",
|
|
85
91
|
status: "active",
|
|
86
92
|
},
|
|
93
|
+
{
|
|
94
|
+
id: 105,
|
|
95
|
+
name: "web4.example.com",
|
|
96
|
+
provider_name: "hetzner",
|
|
97
|
+
ubuntu_version: "24.04",
|
|
98
|
+
ip_address: "203.0.113.24",
|
|
99
|
+
ssh_port: 22,
|
|
100
|
+
timezone: "UTC",
|
|
101
|
+
region: "fsn1",
|
|
102
|
+
size: "cx41",
|
|
103
|
+
disk_space: { total: 120 * GB, available: 80 * GB, used: 40 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
104
|
+
database: null,
|
|
105
|
+
connection_status: "connected",
|
|
106
|
+
reboot_required: false,
|
|
107
|
+
upgrade_required: false,
|
|
108
|
+
created_at: "2024-05-12T12:00:00Z",
|
|
109
|
+
status: "active",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 106,
|
|
113
|
+
name: "web5.example.net",
|
|
114
|
+
provider_name: "digitalocean",
|
|
115
|
+
ubuntu_version: "22.04",
|
|
116
|
+
ip_address: "203.0.113.25",
|
|
117
|
+
ssh_port: 22,
|
|
118
|
+
timezone: "UTC",
|
|
119
|
+
region: "sfo3",
|
|
120
|
+
size: "s-2vcpu-4gb",
|
|
121
|
+
disk_space: { total: 60 * GB, available: 42 * GB, used: 18 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
122
|
+
database: null,
|
|
123
|
+
connection_status: "connected",
|
|
124
|
+
reboot_required: false,
|
|
125
|
+
upgrade_required: false,
|
|
126
|
+
created_at: "2025-09-19T10:30:00Z",
|
|
127
|
+
status: "active",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 107,
|
|
131
|
+
name: "web6.example.com",
|
|
132
|
+
provider_name: "vultr",
|
|
133
|
+
ubuntu_version: "24.04",
|
|
134
|
+
ip_address: "203.0.113.26",
|
|
135
|
+
ssh_port: 22,
|
|
136
|
+
timezone: "UTC",
|
|
137
|
+
region: "lax",
|
|
138
|
+
size: "vc2-4c-8gb",
|
|
139
|
+
disk_space: { total: 100 * GB, available: 75 * GB, used: 25 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
140
|
+
database: null,
|
|
141
|
+
connection_status: "connected",
|
|
142
|
+
reboot_required: false,
|
|
143
|
+
upgrade_required: false,
|
|
144
|
+
created_at: "2023-11-03T08:15:00Z",
|
|
145
|
+
status: "active",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 108,
|
|
149
|
+
name: "web7.example.net",
|
|
150
|
+
provider_name: "linode",
|
|
151
|
+
ubuntu_version: "22.04",
|
|
152
|
+
ip_address: "203.0.113.27",
|
|
153
|
+
ssh_port: 22,
|
|
154
|
+
timezone: "UTC",
|
|
155
|
+
region: "eu-west",
|
|
156
|
+
size: "g6-standard-4",
|
|
157
|
+
disk_space: { total: 100 * GB, available: 45 * GB, used: 55 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
158
|
+
database: null,
|
|
159
|
+
connection_status: "connected",
|
|
160
|
+
reboot_required: true,
|
|
161
|
+
upgrade_required: false,
|
|
162
|
+
created_at: "2024-02-27T09:00:00Z",
|
|
163
|
+
status: "active",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 109,
|
|
167
|
+
name: "web8.example.org",
|
|
168
|
+
provider_name: "hetzner",
|
|
169
|
+
ubuntu_version: "18.04",
|
|
170
|
+
ip_address: "203.0.113.28",
|
|
171
|
+
ssh_port: 22,
|
|
172
|
+
timezone: "UTC",
|
|
173
|
+
region: "hel1",
|
|
174
|
+
size: "cx31",
|
|
175
|
+
disk_space: { total: 40 * GB, available: 2 * GB, used: 38 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
176
|
+
database: null,
|
|
177
|
+
connection_status: "connected",
|
|
178
|
+
reboot_required: false,
|
|
179
|
+
upgrade_required: true,
|
|
180
|
+
created_at: "2021-09-14T11:00:00Z",
|
|
181
|
+
status: "active",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 110,
|
|
185
|
+
name: "web10.example.com",
|
|
186
|
+
provider_name: "digitalocean",
|
|
187
|
+
ubuntu_version: "24.04",
|
|
188
|
+
ip_address: "203.0.113.30",
|
|
189
|
+
ssh_port: 22,
|
|
190
|
+
timezone: "UTC",
|
|
191
|
+
region: "ams3",
|
|
192
|
+
size: "s-4vcpu-8gb",
|
|
193
|
+
disk_space: { total: 160 * GB, available: 90 * GB, used: 70 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
194
|
+
database: null,
|
|
195
|
+
connection_status: "connected",
|
|
196
|
+
reboot_required: false,
|
|
197
|
+
upgrade_required: false,
|
|
198
|
+
created_at: "2024-08-30T14:20:00Z",
|
|
199
|
+
status: "active",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 111,
|
|
203
|
+
name: "web11.example.org",
|
|
204
|
+
provider_name: "vultr",
|
|
205
|
+
ubuntu_version: "24.04",
|
|
206
|
+
ip_address: "203.0.113.31",
|
|
207
|
+
ssh_port: 22,
|
|
208
|
+
timezone: "UTC",
|
|
209
|
+
region: "syd",
|
|
210
|
+
size: "vc2-2c-4gb",
|
|
211
|
+
disk_space: null,
|
|
212
|
+
database: null,
|
|
213
|
+
connection_status: "connecting",
|
|
214
|
+
reboot_required: false,
|
|
215
|
+
upgrade_required: false,
|
|
216
|
+
created_at: "2026-07-08T07:50:00Z",
|
|
217
|
+
status: "active",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 112,
|
|
221
|
+
name: "edge1.example.net",
|
|
222
|
+
provider_name: null,
|
|
223
|
+
ubuntu_version: "22.04",
|
|
224
|
+
ip_address: "203.0.113.32",
|
|
225
|
+
ssh_port: 2222,
|
|
226
|
+
timezone: "UTC",
|
|
227
|
+
region: null,
|
|
228
|
+
size: null,
|
|
229
|
+
disk_space: { total: 80 * GB, available: 71 * GB, used: 9 * GB, updated_at: "2026-07-01T08:00:00Z" },
|
|
230
|
+
database: null,
|
|
231
|
+
connection_status: "connected",
|
|
232
|
+
reboot_required: false,
|
|
233
|
+
upgrade_required: false,
|
|
234
|
+
created_at: "2025-04-02T13:10:00Z",
|
|
235
|
+
status: "active",
|
|
236
|
+
},
|
|
87
237
|
]
|
|
88
238
|
|
|
89
|
-
const
|
|
239
|
+
const HAND_SITES: Site[] = [
|
|
90
240
|
{
|
|
91
241
|
id: 1001,
|
|
92
242
|
server_id: 101,
|
|
@@ -193,6 +343,169 @@ const SITES: Site[] = [
|
|
|
193
343
|
},
|
|
194
344
|
]
|
|
195
345
|
|
|
346
|
+
// --- Generated sites (1007+) ------------------------------------------------
|
|
347
|
+
//
|
|
348
|
+
// One realistic short business-site word per generated site, so domains read as
|
|
349
|
+
// a real-looking fleet rather than "site1.example.com". Words already used by
|
|
350
|
+
// HAND_SITES (shop/blog/docs/app) are excluded to avoid collisions.
|
|
351
|
+
const WORDS = [
|
|
352
|
+
"status", "portal", "members", "support", "academy", "press", "forum", "store",
|
|
353
|
+
"news", "events", "clinic", "studio", "gallery", "market", "travel", "fitness",
|
|
354
|
+
"realty", "law", "dental", "vet", "bakery", "garage", "garden", "kitchen",
|
|
355
|
+
"media", "agency", "consult", "finance", "insure", "charity", "church",
|
|
356
|
+
"school", "camp", "yoga", "spa", "salon", "barber", "tailor", "florist",
|
|
357
|
+
"nursery", "daycare", "tutoring", "coaching", "photography", "videography",
|
|
358
|
+
"catering", "brewery", "winery", "coffee", "bistro", "diner", "grill",
|
|
359
|
+
"pizzeria", "patisserie", "boutique", "jewelry", "antiques", "hardware",
|
|
360
|
+
"plumbing", "electric", "roofing", "landscaping", "painting", "moving",
|
|
361
|
+
"cleaning", "security", "accounting", "marketing", "printing", "repair",
|
|
362
|
+
"rental", "storage", "notary", "locksmith",
|
|
363
|
+
]
|
|
364
|
+
const TLDS = ["com", "org", "net"]
|
|
365
|
+
|
|
366
|
+
// A handful of dates to cycle through deterministically (spread over ~3 years
|
|
367
|
+
// up to "today" in Dev Mode, 2026-07-08) — real-looking site ages without
|
|
368
|
+
// touching Date.now().
|
|
369
|
+
const CREATED_DATES = [
|
|
370
|
+
"2022-09-14T10:00:00Z", "2023-01-22T14:30:00Z", "2023-05-09T08:45:00Z",
|
|
371
|
+
"2023-08-30T16:00:00Z", "2023-11-17T11:20:00Z", "2024-02-06T09:15:00Z",
|
|
372
|
+
"2024-04-19T13:40:00Z", "2024-06-25T10:05:00Z", "2024-09-03T15:50:00Z",
|
|
373
|
+
"2024-12-11T12:00:00Z", "2025-02-14T09:30:00Z", "2025-04-28T17:10:00Z",
|
|
374
|
+
"2025-06-16T08:20:00Z", "2025-08-22T14:00:00Z", "2025-10-05T11:45:00Z",
|
|
375
|
+
"2025-12-01T09:00:00Z", "2026-02-09T13:15:00Z", "2026-04-14T10:30:00Z",
|
|
376
|
+
"2026-05-20T15:40:00Z", "2026-06-10T08:00:00Z",
|
|
377
|
+
]
|
|
378
|
+
|
|
379
|
+
// One "kind" per site, cycled by index — covers every stack the app classifies
|
|
380
|
+
// (Standard WP / Bedrock / Non-WP), plus the health signals the Dashboard,
|
|
381
|
+
// Stacks, and Browser views surface: EOL PHP, disabled HTTPS, disabled/absent
|
|
382
|
+
// backups, pending core/theme/plugin updates, and a non-Bedrock WP site that
|
|
383
|
+
// still deploys via git (a case cloneStackFor's git.repo-precedence handles).
|
|
384
|
+
type SiteKind = (site: { id: number; server_id: number; domain: string; site_user: string; created_at: string }) => Site
|
|
385
|
+
|
|
386
|
+
const KINDS: SiteKind[] = [
|
|
387
|
+
// Standard WP, healthy, current PHP.
|
|
388
|
+
(s) => ({
|
|
389
|
+
...s, php_version: "8.3", public_folder: "/", is_wordpress: true,
|
|
390
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
391
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
392
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
393
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 2, status: "active",
|
|
394
|
+
}),
|
|
395
|
+
// Bedrock, healthy, current PHP.
|
|
396
|
+
(s) => ({
|
|
397
|
+
...s, php_version: "8.2", public_folder: "/web/", is_wordpress: true,
|
|
398
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
399
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
400
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
401
|
+
git: { repo: `git@github.com:example/${s.site_user}-bedrock.git`, branch: "main", deploy_script: "composer install -o --no-dev", push_enabled: true, deployment_url: null },
|
|
402
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 0, status: "active",
|
|
403
|
+
}),
|
|
404
|
+
// Non-WP static site.
|
|
405
|
+
(s) => ({
|
|
406
|
+
...s, php_version: "8.3", public_folder: "/public/", is_wordpress: false,
|
|
407
|
+
https: { enabled: true }, status: "active",
|
|
408
|
+
}),
|
|
409
|
+
// WP on EOL PHP, no backups, plenty of updates pending.
|
|
410
|
+
(s) => ({
|
|
411
|
+
...s, php_version: "7.4", public_folder: "/", is_wordpress: true,
|
|
412
|
+
page_cache: { enabled: false }, https: { enabled: true },
|
|
413
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
414
|
+
backups: { files: false, database: false },
|
|
415
|
+
wp_core_update: true, wp_theme_updates: 2, wp_plugin_updates: 7, status: "active",
|
|
416
|
+
}),
|
|
417
|
+
// Non-WP app (custom PHP app, not WordPress at all).
|
|
418
|
+
(s) => ({
|
|
419
|
+
...s, php_version: "8.1", public_folder: "/public/", is_wordpress: false,
|
|
420
|
+
https: { enabled: true }, status: "active",
|
|
421
|
+
}),
|
|
422
|
+
// Standard WP with HTTPS disabled.
|
|
423
|
+
(s) => ({
|
|
424
|
+
...s, php_version: "8.3", public_folder: "/", is_wordpress: true,
|
|
425
|
+
page_cache: { enabled: true }, https: { enabled: false },
|
|
426
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
427
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
428
|
+
wp_core_update: false, wp_theme_updates: 1, wp_plugin_updates: 1, status: "active",
|
|
429
|
+
}),
|
|
430
|
+
// Bedrock on EOL PHP (8.1 aged out of security support 2025-12-31).
|
|
431
|
+
(s) => ({
|
|
432
|
+
...s, php_version: "8.1", public_folder: "/web/", is_wordpress: true,
|
|
433
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
434
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
435
|
+
backups: { files: true, database: true, retention_period: 7, next_run_time: "2026-07-09T03:00:00Z" },
|
|
436
|
+
git: { repo: `git@github.com:example/${s.site_user}-bedrock.git`, branch: "main", deploy_script: "composer install -o --no-dev", push_enabled: true, deployment_url: null },
|
|
437
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 1, status: "active",
|
|
438
|
+
}),
|
|
439
|
+
// Standard WP, heavy pending updates + EOL PHP.
|
|
440
|
+
(s) => ({
|
|
441
|
+
...s, php_version: "8.0", public_folder: "/", is_wordpress: true,
|
|
442
|
+
page_cache: { enabled: false }, https: { enabled: true },
|
|
443
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
444
|
+
backups: { files: true, database: true, retention_period: 7, next_run_time: "2026-07-09T03:00:00Z" },
|
|
445
|
+
wp_core_update: true, wp_theme_updates: 3, wp_plugin_updates: 11, status: "active",
|
|
446
|
+
}),
|
|
447
|
+
// Fresh Standard WP site, newest PHP, everything current.
|
|
448
|
+
(s) => ({
|
|
449
|
+
...s, php_version: "8.4", public_folder: "/", is_wordpress: true,
|
|
450
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
451
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
452
|
+
backups: { files: true, database: true, retention_period: 30, next_run_time: "2026-07-09T03:00:00Z" },
|
|
453
|
+
wp_core_update: false, wp_theme_updates: 0, wp_plugin_updates: 0, status: "active",
|
|
454
|
+
}),
|
|
455
|
+
// Standard WP (not Bedrock) that still deploys via git.
|
|
456
|
+
(s) => ({
|
|
457
|
+
...s, php_version: "8.3", public_folder: "/", is_wordpress: true,
|
|
458
|
+
page_cache: { enabled: true }, https: { enabled: true },
|
|
459
|
+
database: { id: s.id, user_id: s.id, table_prefix: "wp_" },
|
|
460
|
+
backups: { files: true, database: true, retention_period: 14, next_run_time: "2026-07-09T03:00:00Z" },
|
|
461
|
+
git: { repo: `git@github.com:example/${s.site_user}.git`, branch: "main", deploy_script: null, push_enabled: false, deployment_url: null },
|
|
462
|
+
wp_core_update: false, wp_theme_updates: 1, wp_plugin_updates: 4, status: "active",
|
|
463
|
+
}),
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
// How many generated sites land on each active server. 102 (empty) and 111
|
|
467
|
+
// (still connecting) deliberately get none — see the SERVERS list above.
|
|
468
|
+
const GENERATED_SITE_PLAN: { serverId: number; count: number }[] = [
|
|
469
|
+
{ serverId: 101, count: 3 },
|
|
470
|
+
{ serverId: 103, count: 3 },
|
|
471
|
+
{ serverId: 104, count: 3 },
|
|
472
|
+
{ serverId: 105, count: 12 },
|
|
473
|
+
{ serverId: 106, count: 3 },
|
|
474
|
+
{ serverId: 107, count: 15 },
|
|
475
|
+
{ serverId: 108, count: 8 },
|
|
476
|
+
{ serverId: 109, count: 5 },
|
|
477
|
+
{ serverId: 110, count: 20 },
|
|
478
|
+
{ serverId: 112, count: 2 },
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
function generateSites(): Site[] {
|
|
482
|
+
const out: Site[] = []
|
|
483
|
+
let wordIdx = 0
|
|
484
|
+
let id = 1007
|
|
485
|
+
for (const { serverId, count } of GENERATED_SITE_PLAN) {
|
|
486
|
+
for (let i = 0; i < count; i++) {
|
|
487
|
+
const word = WORDS[wordIdx % WORDS.length]!
|
|
488
|
+
const tld = TLDS[wordIdx % TLDS.length]!
|
|
489
|
+
const domain = `${word}.example.${tld}`
|
|
490
|
+
const kind = KINDS[wordIdx % KINDS.length]!
|
|
491
|
+
out.push(
|
|
492
|
+
kind({
|
|
493
|
+
id,
|
|
494
|
+
server_id: serverId,
|
|
495
|
+
domain,
|
|
496
|
+
site_user: word,
|
|
497
|
+
created_at: CREATED_DATES[wordIdx % CREATED_DATES.length]!,
|
|
498
|
+
}),
|
|
499
|
+
)
|
|
500
|
+
wordIdx++
|
|
501
|
+
id++
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return out
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const SITES: Site[] = [...HAND_SITES, ...generateSites()]
|
|
508
|
+
|
|
196
509
|
const EVENTS: Event[] = [
|
|
197
510
|
{
|
|
198
511
|
id: 9001,
|
|
@@ -227,6 +540,182 @@ const EVENTS: Event[] = [
|
|
|
227
540
|
started_at: "2026-06-27T04:00:02Z",
|
|
228
541
|
finished_at: "2026-06-27T04:01:15Z",
|
|
229
542
|
},
|
|
543
|
+
{
|
|
544
|
+
id: 9004,
|
|
545
|
+
initiated_by: "dev-mode",
|
|
546
|
+
server_id: 111,
|
|
547
|
+
name: "server.provisioned",
|
|
548
|
+
status: "running",
|
|
549
|
+
output: "Provisioning web11.example.org on Vultr (syd)…",
|
|
550
|
+
created_at: "2026-07-08T07:50:00Z",
|
|
551
|
+
started_at: "2026-07-08T07:50:03Z",
|
|
552
|
+
finished_at: null,
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
id: 9005,
|
|
556
|
+
initiated_by: "dev-mode",
|
|
557
|
+
server_id: 107,
|
|
558
|
+
name: "site.https.enabled",
|
|
559
|
+
status: "failed",
|
|
560
|
+
output: "HTTPS request failed for realty.example.com — DNS does not yet resolve to this server",
|
|
561
|
+
created_at: "2026-07-07T15:22:00Z",
|
|
562
|
+
started_at: "2026-07-07T15:22:01Z",
|
|
563
|
+
finished_at: "2026-07-07T15:22:18Z",
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: 9006,
|
|
567
|
+
initiated_by: "dev-mode",
|
|
568
|
+
server_id: 105,
|
|
569
|
+
name: "site.php_version.updated",
|
|
570
|
+
status: "deployed",
|
|
571
|
+
output: "PHP updated to 8.4 for coffee.example.net",
|
|
572
|
+
created_at: "2026-07-06T10:05:00Z",
|
|
573
|
+
started_at: "2026-07-06T10:05:04Z",
|
|
574
|
+
finished_at: "2026-07-06T10:05:39Z",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
id: 9007,
|
|
578
|
+
initiated_by: "dev-mode",
|
|
579
|
+
server_id: 110,
|
|
580
|
+
name: "site.domains.updated",
|
|
581
|
+
status: "deployed",
|
|
582
|
+
output: "Added domain www.marketing.example.com",
|
|
583
|
+
created_at: "2026-07-05T13:40:00Z",
|
|
584
|
+
started_at: "2026-07-05T13:40:02Z",
|
|
585
|
+
finished_at: "2026-07-05T13:40:27Z",
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: 9008,
|
|
589
|
+
initiated_by: "dev-mode",
|
|
590
|
+
server_id: 108,
|
|
591
|
+
name: "server.service.mysql.restarted",
|
|
592
|
+
status: "deployed",
|
|
593
|
+
output: "mysql restarted on web7.example.net",
|
|
594
|
+
created_at: "2026-07-04T22:10:00Z",
|
|
595
|
+
started_at: "2026-07-04T22:10:01Z",
|
|
596
|
+
finished_at: "2026-07-04T22:10:12Z",
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
id: 9009,
|
|
600
|
+
initiated_by: "dev-mode",
|
|
601
|
+
server_id: 109,
|
|
602
|
+
name: "site.object_cache.purged",
|
|
603
|
+
status: "deployed",
|
|
604
|
+
output: "Object cache purged for repair.example.net",
|
|
605
|
+
created_at: "2026-07-04T09:00:00Z",
|
|
606
|
+
started_at: "2026-07-04T09:00:01Z",
|
|
607
|
+
finished_at: "2026-07-04T09:00:06Z",
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: 9010,
|
|
611
|
+
initiated_by: "dev-mode",
|
|
612
|
+
server_id: 106,
|
|
613
|
+
name: "site.https.disabled",
|
|
614
|
+
status: "deployed",
|
|
615
|
+
output: "HTTPS disabled for insure.example.org pending certificate reissue",
|
|
616
|
+
created_at: "2026-07-02T18:30:00Z",
|
|
617
|
+
started_at: "2026-07-02T18:30:02Z",
|
|
618
|
+
finished_at: "2026-07-02T18:30:20Z",
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
id: 9011,
|
|
622
|
+
initiated_by: "dev-mode",
|
|
623
|
+
server_id: 107,
|
|
624
|
+
name: "server.rebooted",
|
|
625
|
+
status: "deployed",
|
|
626
|
+
output: "Server rebooted successfully",
|
|
627
|
+
created_at: "2026-06-28T04:00:00Z",
|
|
628
|
+
started_at: "2026-06-28T04:00:02Z",
|
|
629
|
+
finished_at: "2026-06-28T04:01:08Z",
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: 9012,
|
|
633
|
+
initiated_by: "dev-mode",
|
|
634
|
+
server_id: 110,
|
|
635
|
+
name: "site.created",
|
|
636
|
+
status: "deployed",
|
|
637
|
+
output: "Site salon.example.com created",
|
|
638
|
+
created_at: "2026-06-24T11:15:00Z",
|
|
639
|
+
started_at: "2026-06-24T11:15:02Z",
|
|
640
|
+
finished_at: "2026-06-24T11:16:40Z",
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: 9013,
|
|
644
|
+
initiated_by: "dev-mode",
|
|
645
|
+
server_id: 105,
|
|
646
|
+
name: "server.service.nginx.restarted",
|
|
647
|
+
status: "deployed",
|
|
648
|
+
output: "nginx restarted on web4.example.com",
|
|
649
|
+
created_at: "2026-06-20T06:45:00Z",
|
|
650
|
+
started_at: "2026-06-20T06:45:01Z",
|
|
651
|
+
finished_at: "2026-06-20T06:45:09Z",
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: 9014,
|
|
655
|
+
initiated_by: "dev-mode",
|
|
656
|
+
server_id: 109,
|
|
657
|
+
name: "server.upgrade_required",
|
|
658
|
+
status: "deployed",
|
|
659
|
+
output: "Security updates available on web8.example.org (Ubuntu 18.04, EOL)",
|
|
660
|
+
created_at: "2026-06-18T05:00:00Z",
|
|
661
|
+
started_at: "2026-06-18T05:00:00Z",
|
|
662
|
+
finished_at: "2026-06-18T05:00:00Z",
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: 9015,
|
|
666
|
+
initiated_by: "dev-mode",
|
|
667
|
+
server_id: 112,
|
|
668
|
+
name: "site.cache.purged",
|
|
669
|
+
status: "deployed",
|
|
670
|
+
output: "Page cache purged for notary.example.com",
|
|
671
|
+
created_at: "2026-06-15T14:00:00Z",
|
|
672
|
+
started_at: "2026-06-15T14:00:01Z",
|
|
673
|
+
finished_at: "2026-06-15T14:00:07Z",
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
id: 9016,
|
|
677
|
+
initiated_by: "dev-mode",
|
|
678
|
+
server_id: 108,
|
|
679
|
+
name: "site.php_version.updated",
|
|
680
|
+
status: "deployed",
|
|
681
|
+
output: "PHP updated to 8.2 for tailor.example.net",
|
|
682
|
+
created_at: "2026-06-10T09:20:00Z",
|
|
683
|
+
started_at: "2026-06-10T09:20:03Z",
|
|
684
|
+
finished_at: "2026-06-10T09:20:51Z",
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
id: 9017,
|
|
688
|
+
initiated_by: "dev-mode",
|
|
689
|
+
server_id: 106,
|
|
690
|
+
name: "server.provisioned",
|
|
691
|
+
status: "deployed",
|
|
692
|
+
output: "Server web5.example.net provisioned",
|
|
693
|
+
created_at: "2025-09-19T10:30:00Z",
|
|
694
|
+
started_at: "2025-09-19T10:30:04Z",
|
|
695
|
+
finished_at: "2025-09-19T10:34:12Z",
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
id: 9018,
|
|
699
|
+
initiated_by: "dev-mode",
|
|
700
|
+
server_id: 105,
|
|
701
|
+
name: "server.provisioned",
|
|
702
|
+
status: "deployed",
|
|
703
|
+
output: "Server web4.example.com provisioned",
|
|
704
|
+
created_at: "2024-05-12T12:00:00Z",
|
|
705
|
+
started_at: "2024-05-12T12:00:05Z",
|
|
706
|
+
finished_at: "2024-05-12T12:04:30Z",
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
id: 9019,
|
|
710
|
+
initiated_by: "dev-mode",
|
|
711
|
+
server_id: 112,
|
|
712
|
+
name: "server.service.redis.restarted",
|
|
713
|
+
status: "deployed",
|
|
714
|
+
output: "redis restarted on edge1.example.net",
|
|
715
|
+
created_at: "2024-01-08T20:00:00Z",
|
|
716
|
+
started_at: "2024-01-08T20:00:01Z",
|
|
717
|
+
finished_at: "2024-01-08T20:00:06Z",
|
|
718
|
+
},
|
|
230
719
|
]
|
|
231
720
|
|
|
232
721
|
function clone<T>(value: T): T {
|
package/src/lib/serverClone.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// that shape the Bedrock branch).
|
|
12
12
|
|
|
13
13
|
import type { Server } from "../api/types.ts"
|
|
14
|
+
import { wpCliResolveScript } from "./wpCli.ts"
|
|
14
15
|
|
|
15
16
|
const SSH_OPTS = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new"]
|
|
16
17
|
|
|
@@ -700,6 +701,7 @@ export interface VerifySpec {
|
|
|
700
701
|
publicFolder?: string // configured public folder — fallback when the pull's rels are unknown
|
|
701
702
|
sourceWebrootRel?: string // DETECTED source layout from the pull ("" = files root)
|
|
702
703
|
destWebrootRel?: string // the CLONE's WP dir from the pull (post-normalization)
|
|
704
|
+
phpVersion?: string | null // the site's configured PHP version — see wpCli.ts
|
|
703
705
|
}
|
|
704
706
|
|
|
705
707
|
// One round-trip per side: collect labeled wp-cli facts as `key=value` lines.
|
|
@@ -707,10 +709,14 @@ export interface VerifySpec {
|
|
|
707
709
|
// stalled outbound call once ate the whole budget and cut the facts off mid-script) —
|
|
708
710
|
// and the flags are identical on both sides, so every count stays comparable. The
|
|
709
711
|
// trailing `eof` sentinel is how verifyClone tells "read got cut off" from "differs".
|
|
710
|
-
|
|
712
|
+
// wp-cli is pinned to the site's OWN configured PHP-CLI (wpCliResolveScript) rather
|
|
713
|
+
// than bare `wp` — see wpCli.ts for why (a server's system-default PHP can silently
|
|
714
|
+
// differ from, and be missing extensions present in, the site's actual version).
|
|
715
|
+
async function wpFacts(ctx: SudoCtx, root: string, user: string, phpVersion?: string | null): Promise<Record<string, string>> {
|
|
711
716
|
const script = [
|
|
712
717
|
`cd ${shq(root)} 2>/dev/null || exit 0`,
|
|
713
|
-
|
|
718
|
+
wpCliResolveScript(phpVersion),
|
|
719
|
+
`u() { sudo -u ${shq(user)} -H "$PHP" "$WP" --skip-plugins --skip-themes "$@" 2>/dev/null; }`,
|
|
714
720
|
`echo "core=$(u core version)"`,
|
|
715
721
|
`echo "posts=$(u post list --post_type=any --format=count)"`,
|
|
716
722
|
`echo "pages=$(u post list --post_type=page --format=count)"`,
|
|
@@ -753,8 +759,8 @@ export async function verifyClone(source: SudoCtx, dest: SudoCtx, spec: VerifySp
|
|
|
753
759
|
const srcDir = dirFor(spec.sourceWebrootRel)
|
|
754
760
|
const destDir = dirFor(spec.destWebrootRel)
|
|
755
761
|
const [sf, cf, http] = await Promise.all([
|
|
756
|
-
wpFacts(source, srcDir, spec.sourceSiteUser),
|
|
757
|
-
wpFacts(dest, destDir, spec.destSiteUser),
|
|
762
|
+
wpFacts(source, srcDir, spec.sourceSiteUser, spec.phpVersion),
|
|
763
|
+
wpFacts(dest, destDir, spec.destSiteUser, spec.phpVersion),
|
|
758
764
|
curlStatus(spec.domain, spec.destIp),
|
|
759
765
|
])
|
|
760
766
|
// A missing tail sentinel means the facts script was cut off partway (timeout, a
|
|
@@ -764,6 +770,22 @@ export async function verifyClone(source: SudoCtx, dest: SudoCtx, spec: VerifySp
|
|
|
764
770
|
const side = sf.eof !== "1" ? "source" : "clone"
|
|
765
771
|
throw new Error(`couldn't read the ${side}'s WordPress facts (the wp-cli read was cut off) — press v to re-run`)
|
|
766
772
|
}
|
|
773
|
+
// `core` succeeding (a file read, no DB) while EVERY database-backed fact comes
|
|
774
|
+
// back empty is a distinct signature from a real content difference (which shows
|
|
775
|
+
// differing NON-empty values, not blanks) — it means wp-cli's PHP-CLI couldn't
|
|
776
|
+
// reach the database on that side, commonly a missing `mysqli` extension for
|
|
777
|
+
// whatever PHP version it resolved to (see wpCli.ts). Surface that plainly
|
|
778
|
+
// instead of a wall of misleading ✕ rows implying the clone lost data.
|
|
779
|
+
const dbFields = ["posts", "pages", "users", "plugins", "siteurl", "home"] as const
|
|
780
|
+
const looksBroken = (f: Record<string, string>) => !!f.core && dbFields.every((k) => !f[k])
|
|
781
|
+
const srcBroken = looksBroken(sf)
|
|
782
|
+
const cloneBroken = looksBroken(cf)
|
|
783
|
+
if (srcBroken || cloneBroken) {
|
|
784
|
+
const side = srcBroken && cloneBroken ? "both the source and the clone" : srcBroken ? "the source" : "the clone"
|
|
785
|
+
throw new Error(
|
|
786
|
+
`wp-cli can't reach ${side}'s database over PHP (commonly a missing "mysqli" extension for whatever PHP-CLI it resolved to) — this looks like a server/PHP-CLI environment issue, not an actual data difference.`,
|
|
787
|
+
)
|
|
788
|
+
}
|
|
767
789
|
const checks: VerifyCheck[] = []
|
|
768
790
|
checks.push({ key: "http", label: "Clone serves (HTTP)", source: "—", clone: http, ok: /^[23]\d\d$/.test(http) })
|
|
769
791
|
const cmp = (key: string, label: string) => {
|
package/src/lib/stack.ts
CHANGED
|
@@ -49,6 +49,29 @@ export function effectiveStack(site: Site, probeKind?: ProbeKind | null): Stack
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Which pull the clone wizard runs for a site — the SAME effectiveStack a
|
|
53
|
+
// probe corrects elsewhere (Stacks tab, Browser), so an API-mislabeled site
|
|
54
|
+
// (is_wordpress=false but really WordPress — confirmed live against real
|
|
55
|
+
// client sites, 2026-07-08) gets a real database pull once probed, instead
|
|
56
|
+
// of silently defaulting to a files-only copy. git.repo still takes
|
|
57
|
+
// precedence for Bedrock detection (the pull needs the repo URL, which a
|
|
58
|
+
// probe alone can't supply) — is_wordpress is unreliable for git sites, so
|
|
59
|
+
// absent a probe (or a WP-family one) git.repo remains the best available
|
|
60
|
+
// signal. But git.repo alone only proves "deployed via git," not "is
|
|
61
|
+
// Bedrock" — a git-deployed STATIC site (confirmed live, 2026-07-09: a
|
|
62
|
+
// client's git-deployed static site, repo literally named
|
|
63
|
+
// "*-static-site") would otherwise be routed through a Bedrock pull
|
|
64
|
+
// (composer install, wp-cli) it can't survive. A CONCLUSIVE non-WordPress
|
|
65
|
+
// probe (whmcs/laravel/static) overrides git.repo; a probe saying
|
|
66
|
+
// "bedrock"/"wordpress", or no probe at all, still trusts git.repo.
|
|
67
|
+
export function cloneStackFor(site: Site, probeKind?: ProbeKind | null): "wp" | "bedrock" | "files" {
|
|
68
|
+
if (site.git?.repo) {
|
|
69
|
+
if (probeKind === "whmcs" || probeKind === "laravel" || probeKind === "static") return "files"
|
|
70
|
+
return "bedrock"
|
|
71
|
+
}
|
|
72
|
+
return effectiveStack(site, probeKind) === "Standard WP" ? "wp" : "files"
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
// `onSelection` brightens the colors that read poorly on the focused
|
|
53
76
|
// (bright-green) selection background (green-on-green Bedrock, the faint grey).
|
|
54
77
|
export function stackColor(stack: Stack, onSelection = false): string {
|
package/src/lib/wpCli.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Resolve wp-cli through the SITE's own configured PHP-CLI, not the bare `wp`
|
|
2
|
+
// command. wp-cli's installed binary shebangs `#!/usr/bin/env php`, which
|
|
3
|
+
// resolves through PATH to the SERVER's system-DEFAULT PHP — which drifts
|
|
4
|
+
// toward whatever version was installed most recently, independent of any
|
|
5
|
+
// individual site's configured version, and can be missing extensions that
|
|
6
|
+
// version's CLI SAPI never got configured with.
|
|
7
|
+
//
|
|
8
|
+
// Verified live 2026-07-09 on web4.wenmarkdigital.com: the system default had
|
|
9
|
+
// drifted to 8.4, whose CLI lacked `mysqli` (its FPM pool likely never needed
|
|
10
|
+
// it — no site there runs 8.4), while lp.anchoredconstructiontn.com's actual
|
|
11
|
+
// configured version (8.1) has a fully working php8.1-cli. Bare `wp` silently
|
|
12
|
+
// ran under 8.4 and failed on anything touching $wpdb (post/user/plugin list,
|
|
13
|
+
// option get) — while `wp core version`, and `wp db export/import/check/size`
|
|
14
|
+
// (they shell out to native mysql client tools, not through mysqli) were
|
|
15
|
+
// unaffected. Confirmed the fix directly: `/usr/bin/php8.1 /usr/local/bin/wp
|
|
16
|
+
// post list ...` returned correct data where bare `wp` errored.
|
|
17
|
+
//
|
|
18
|
+
// Only applies to REMOTE (SSH) wp-cli calls against SpinupWP-managed servers,
|
|
19
|
+
// which have this specific multi-PHP-version-drift shape. Local dev-machine
|
|
20
|
+
// wp-cli (e.g. dbSync.ts's prod→local pull) is a different environment with
|
|
21
|
+
// its own PHP entirely and is not affected — don't apply this there.
|
|
22
|
+
|
|
23
|
+
// Emits POSIX shell that resolves $WP (the wp-cli binary) and $PHP (the
|
|
24
|
+
// version-pinned interpreter, falling back to the bare `php` on PATH if the
|
|
25
|
+
// versioned binary isn't installed — never worse than today's behavior).
|
|
26
|
+
// Callers invoke `"$PHP" "$WP" ...` in place of bare `wp ...`.
|
|
27
|
+
export function wpCliResolveScript(phpVersion?: string | null): string {
|
|
28
|
+
const phpBin = phpVersion ? `php${phpVersion}` : "php"
|
|
29
|
+
return [`WP=$(command -v wp 2>/dev/null || echo /usr/local/bin/wp)`, `PHP=$(command -v ${phpBin} 2>/dev/null || command -v php)`].join("\n")
|
|
30
|
+
}
|