@rxtx4816/cockpit-plugin-base-react 1.0.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 (41) hide show
  1. package/eslint.config.base.js +85 -0
  2. package/package.json +116 -0
  3. package/scripts/test-vm.sh +608 -0
  4. package/src/bootstrap.tsx +7 -0
  5. package/src/cockpit.d.ts +63 -0
  6. package/src/components/ConfirmDialog.test.tsx +61 -0
  7. package/src/components/ConfirmDialog.tsx +65 -0
  8. package/src/components/ErrorBoundary.test.tsx +50 -0
  9. package/src/components/ErrorBoundary.tsx +30 -0
  10. package/src/components/HelpPopover.tsx +30 -0
  11. package/src/components/LogViewer.tsx +108 -0
  12. package/src/components/StatusBadge.test.tsx +32 -0
  13. package/src/components/StatusBadge.tsx +18 -0
  14. package/src/components/ToastProvider.css +20 -0
  15. package/src/components/ToastProvider.test.tsx +61 -0
  16. package/src/components/ToastProvider.tsx +76 -0
  17. package/src/components/index.ts +8 -0
  18. package/src/css.d.ts +4 -0
  19. package/src/dark-theme.ts +30 -0
  20. package/src/hooks/useAsyncAction.test.ts +59 -0
  21. package/src/hooks/useAsyncAction.ts +31 -0
  22. package/src/hooks/useAsyncStream.test.ts +122 -0
  23. package/src/hooks/useAsyncStream.ts +94 -0
  24. package/src/hooks/useAutoRefresh.test.ts +106 -0
  25. package/src/hooks/useAutoRefresh.ts +23 -0
  26. package/src/hooks/useConfirmAction.test.ts +68 -0
  27. package/src/hooks/useConfirmAction.ts +35 -0
  28. package/src/hooks/usePollingFetch.ts +41 -0
  29. package/src/i18n.ts +51 -0
  30. package/src/index.ts +11 -0
  31. package/src/systemd/ServiceControl.tsx +172 -0
  32. package/src/systemd/api.test.ts +83 -0
  33. package/src/systemd/api.ts +36 -0
  34. package/src/systemd/index.ts +5 -0
  35. package/src/systemd/types.ts +1 -0
  36. package/src/systemd/useServiceStatus.test.ts +96 -0
  37. package/src/systemd/useServiceStatus.ts +29 -0
  38. package/src/testing/helpers.ts +30 -0
  39. package/src/testing/setup.ts +57 -0
  40. package/tsconfig.base.json +17 -0
  41. package/vitest.config.base.ts +47 -0
@@ -0,0 +1,608 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/test-vm.sh — Generic QEMU VM test harness for Cockpit plugins
3
+ #
4
+ # Spins up cloud VMs (Arch, Debian, Fedora by default) for end-to-end testing.
5
+ #
6
+ # Usage from your plugin directory:
7
+ # ./node_modules/@rxtx4816/cockpit-plugin-base/scripts/test-vm.sh <command> [vm ...]
8
+ #
9
+ # Or add to your package.json scripts:
10
+ # "vm": "node_modules/@rxtx4816/cockpit-plugin-base/scripts/test-vm.sh"
11
+ #
12
+ # Each plugin provides a scripts/test-vm.config.sh that customises:
13
+ # - PLUGIN_NAME, MOUNT_TAG, INSTALL_PATH
14
+ # - ALL_VMS, SSH_BASE, COCKPIT_BASE
15
+ # - extra_packages(distro) — echo packages to install
16
+ # - extra_runcmd(vm) — echo cloud-init runcmd lines
17
+ # - pre_staged_files(vm) — echo cloud-init write_files blocks
18
+ #
19
+ # Dependencies (Arch): qemu-full cloud-image-utils wget
20
+ # sudo pacman -S qemu-full cloud-image-utils wget
21
+ #
22
+ # Quick start:
23
+ # npm run build
24
+ # npm run vm download debian
25
+ # npm run vm start debian
26
+ # npm run vm wait debian
27
+ # # Open https://localhost:$COCKPIT_BASE — login: test / test
28
+
29
+ set -euo pipefail
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ PROJECT_DIR="$(pwd)"
33
+ VM_DIR="$PROJECT_DIR/.vms"
34
+ DIST_DIR="$PROJECT_DIR/src"
35
+
36
+ # ── Defaults (overridden by test-vm.config.sh) ────────────────────────────────
37
+
38
+ PLUGIN_NAME="cockpit-plugin"
39
+ MOUNT_TAG="cockpit_plugin"
40
+ INSTALL_PATH="/usr/share/cockpit/cockpit-plugin"
41
+
42
+ ALL_VMS=(arch debian fedora)
43
+ SSH_BASE=2220
44
+ COCKPIT_BASE=9090
45
+
46
+ VM_MEM="${VM_MEM:-1024}"
47
+ VM_CPUS="${VM_CPUS:-2}"
48
+ VM_DISK_SIZE="${VM_DISK_SIZE:-12G}"
49
+
50
+ # Default no-op hooks — plugins override these
51
+ extra_packages() { :; }
52
+ extra_runcmd() { :; }
53
+ pre_staged_files() { :; }
54
+
55
+ # ── Load plugin config ────────────────────────────────────────────────────────
56
+
57
+ CONFIG_FILE="$PROJECT_DIR/scripts/test-vm.config.sh"
58
+ if [[ -f "$CONFIG_FILE" ]]; then
59
+ # shellcheck source=/dev/null
60
+ source "$CONFIG_FILE"
61
+ else
62
+ echo "WARNING: $CONFIG_FILE not found — using defaults (PLUGIN_NAME=$PLUGIN_NAME)"
63
+ fi
64
+
65
+ # ── Cloud image URLs ──────────────────────────────────────────────────────────
66
+
67
+ ARCH_IMAGE_URL="https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2"
68
+ DEBIAN_IMAGE_URL="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2"
69
+ FEDORA_VERSION="41"
70
+ FEDORA_BUILD="1.4"
71
+ FEDORA_IMAGE_URL="https://download.fedoraproject.org/pub/fedora/linux/releases/${FEDORA_VERSION}/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-${FEDORA_VERSION}-${FEDORA_BUILD}.x86_64.qcow2"
72
+
73
+ # ── Helpers ───────────────────────────────────────────────────────────────────
74
+
75
+ die() { echo "ERROR: $*" >&2; exit 1; }
76
+ info() { echo "==> $*"; }
77
+ ok() { echo " ✓ $*"; }
78
+
79
+ usage() {
80
+ cat <<EOF
81
+ Usage: $(basename "$0") <command> [vm ...]
82
+
83
+ VM identifiers: ${ALL_VMS[*]}
84
+
85
+ Commands:
86
+ download [vm|all] Download base cloud images
87
+ build Run npm run build
88
+ start [vm ...] Start VM(s) in background
89
+ wait <vm> Block until cloud-init finishes (~2-5 min first boot)
90
+ stop [vm ...] Stop VM(s)
91
+ status Show all VMs with ports and running state
92
+ ssh <vm> Open SSH session
93
+ logs <vm> Tail VM serial console
94
+ clean [vm ...] Wipe disk + state (base image kept)
95
+ rebuild [vm ...] clean + start in one step
96
+ reset [vm ...] Remove all VM files including base image
97
+
98
+ Ports (Cockpit / SSH):
99
+ $(for i in "${!ALL_VMS[@]}"; do
100
+ vm="${ALL_VMS[$i]}"
101
+ printf " %-18s → https://localhost:%d ssh -p %d test@localhost\n" \
102
+ "$vm" "$((COCKPIT_BASE + i))" "$((SSH_BASE + i))"
103
+ done)
104
+
105
+ Login: test / test (your ~/.ssh/id_*.pub is also injected if found)
106
+
107
+ Environment overrides:
108
+ VM_MEM=2048 VM_CPUS=2 VM_DISK_SIZE=12G
109
+ CONFIG: $CONFIG_FILE
110
+ EOF
111
+ exit 1
112
+ }
113
+
114
+ check_deps() {
115
+ local missing=()
116
+ for cmd in qemu-system-x86_64 qemu-img wget; do
117
+ command -v "$cmd" &>/dev/null || missing+=("$cmd")
118
+ done
119
+ if ! command -v cloud-localds &>/dev/null \
120
+ && ! command -v genisoimage &>/dev/null \
121
+ && ! command -v mkisofs &>/dev/null; then
122
+ missing+=("cloud-localds (cloud-image-utils) OR genisoimage")
123
+ fi
124
+ [[ ${#missing[@]} -eq 0 ]] || {
125
+ echo "Missing dependencies:"
126
+ printf ' %s\n' "${missing[@]}"
127
+ echo ""
128
+ echo "Install with: sudo pacman -S qemu-full cloud-image-utils wget"
129
+ exit 1
130
+ }
131
+ }
132
+
133
+ vm_index() {
134
+ local vm="$1"
135
+ for i in "${!ALL_VMS[@]}"; do
136
+ [[ "${ALL_VMS[$i]}" == "$vm" ]] && { echo "$i"; return; }
137
+ done
138
+ die "Unknown VM '$vm'. Valid: ${ALL_VMS[*]}"
139
+ }
140
+
141
+ ssh_port() { echo $((SSH_BASE + $(vm_index "$1"))); }
142
+ cockpit_port() { echo $((COCKPIT_BASE + $(vm_index "$1"))); }
143
+
144
+ vm_distro() {
145
+ local vm="$1"
146
+ # Distro is the first component before any '-' (arch, debian, fedora)
147
+ echo "${vm%%-*}"
148
+ }
149
+
150
+ pid_file() { echo "$VM_DIR/$1/qemu.pid"; }
151
+ disk_img() { echo "$VM_DIR/$1/disk.qcow2"; }
152
+ base_img() { local d; d="$(vm_distro "$1")"; echo "$VM_DIR/$d/base.qcow2"; }
153
+ seed_iso() { echo "$VM_DIR/$1/seed.iso"; }
154
+ console_log() { echo "$VM_DIR/$1/console.log"; }
155
+
156
+ is_running() {
157
+ local pf; pf="$(pid_file "$1")"
158
+ [[ -f "$pf" ]] && kill -0 "$(cat "$pf")" 2>/dev/null
159
+ }
160
+
161
+ resolve_vms() {
162
+ [[ $# -eq 0 ]] && { echo "${ALL_VMS[@]}"; return; }
163
+ local result=() seen=() out=()
164
+ for arg in "$@"; do
165
+ if [[ "$arg" == "all" ]]; then
166
+ result+=("${ALL_VMS[@]}")
167
+ else
168
+ # Accept exact VM names or distro names (which expand to all VMs for that distro)
169
+ local matched=0
170
+ for vm in "${ALL_VMS[@]}"; do
171
+ if [[ "$vm" == "$arg" || "$(vm_distro "$vm")" == "$arg" ]]; then
172
+ result+=("$vm"); matched=1
173
+ fi
174
+ done
175
+ [[ $matched -eq 0 ]] && die "Unknown VM or shortcut: '$arg'. Valid: ${ALL_VMS[*]} all"
176
+ fi
177
+ done
178
+ for v in "${result[@]}"; do
179
+ [[ " ${seen[*]} " == *" $v "* ]] && continue
180
+ seen+=("$v"); out+=("$v")
181
+ done
182
+ echo "${out[@]}"
183
+ }
184
+
185
+ resolve_distros() {
186
+ [[ $# -eq 0 ]] && {
187
+ local seen=() out=()
188
+ for vm in "${ALL_VMS[@]}"; do
189
+ local d; d="$(vm_distro "$vm")"
190
+ [[ " ${seen[*]} " == *" $d "* ]] && continue
191
+ seen+=("$d"); out+=("$d")
192
+ done
193
+ echo "${out[@]}"
194
+ return
195
+ }
196
+ local result=() seen=() out=()
197
+ for arg in "$@"; do
198
+ case "$arg" in
199
+ all) for vm in "${ALL_VMS[@]}"; do result+=("$(vm_distro "$vm")"); done ;;
200
+ *) result+=("$(vm_distro "$arg")") ;;
201
+ esac
202
+ done
203
+ for v in "${result[@]}"; do
204
+ [[ " ${seen[*]} " == *" $v "* ]] && continue
205
+ seen+=("$v"); out+=("$v")
206
+ done
207
+ echo "${out[@]}"
208
+ }
209
+
210
+ find_ssh_pubkey() {
211
+ for key in ~/.ssh/id_ed25519.pub ~/.ssh/id_rsa.pub ~/.ssh/id_ecdsa.pub; do
212
+ [[ -f "$key" ]] && { cat "$key"; return; }
213
+ done
214
+ echo ""
215
+ }
216
+
217
+ make_seed_iso() {
218
+ local iso="$1" userdata="$2" metadata="$3"
219
+ if command -v cloud-localds &>/dev/null; then
220
+ cloud-localds "$iso" "$userdata" "$metadata"
221
+ elif command -v genisoimage &>/dev/null; then
222
+ genisoimage -output "$iso" -volid cidata -joliet -rock "$userdata" "$metadata" 2>/dev/null
223
+ else
224
+ mkisofs -output "$iso" -volid cidata -joliet -rock "$userdata" "$metadata" 2>/dev/null
225
+ fi
226
+ }
227
+
228
+ qemu_accel_args() {
229
+ if [[ -r /dev/kvm ]]; then
230
+ echo "-machine type=q35,accel=kvm -cpu host"
231
+ else
232
+ info "WARNING: /dev/kvm not accessible — running without KVM (will be slow)"
233
+ echo "-machine type=q35"
234
+ fi
235
+ }
236
+
237
+ # ── cloud-init user-data ──────────────────────────────────────────────────────
238
+
239
+ generate_userdata() {
240
+ local vm="$1" ssh_pubkey="$2" outfile="$3"
241
+ local distro; distro="$(vm_distro "$vm")"
242
+
243
+ local group
244
+ case "$distro" in
245
+ arch|fedora) group="wheel" ;;
246
+ debian) group="sudo" ;;
247
+ *) group="sudo" ;;
248
+ esac
249
+
250
+ local ssh_keys_block=" ssh_authorized_keys: []"
251
+ [[ -n "$ssh_pubkey" ]] && ssh_keys_block=" ssh_authorized_keys:
252
+ - ${ssh_pubkey}"
253
+
254
+ # ── header ──────────────────────────────────────────────────────────────────
255
+ cat > "$outfile" <<YAML
256
+ #cloud-config
257
+ hostname: ${vm}-test
258
+
259
+ users:
260
+ - name: test
261
+ groups: ${group}
262
+ sudo: ALL=(ALL) NOPASSWD:ALL
263
+ lock_passwd: false
264
+ ${ssh_keys_block}
265
+
266
+ chpasswd:
267
+ list: |
268
+ test:test
269
+ expire: false
270
+
271
+ package_update: true
272
+ package_upgrade: false
273
+ packages:
274
+ - cockpit
275
+ YAML
276
+
277
+ # Plugin-specific packages ($vm passed as 2nd arg so scenario-aware plugins can use it)
278
+ local pkg
279
+ pkg="$(extra_packages "$distro" "$vm")"
280
+ if [[ -n "$pkg" ]]; then
281
+ while IFS= read -r line; do
282
+ [[ -n "$line" ]] && printf ' - %s\n' "$line" >> "$outfile"
283
+ done <<< "$pkg"
284
+ fi
285
+
286
+ # ── write_files ──────────────────────────────────────────────────────────────
287
+ cat >> "$outfile" <<YAML
288
+
289
+ write_files:
290
+ - path: /etc/modules-load.d/9p.conf
291
+ content: |
292
+ 9p
293
+ 9pnet
294
+ 9pnet_virtio
295
+ YAML
296
+
297
+ # Plugin-specific pre-staged files
298
+ pre_staged_files "$vm" >> "$outfile" || true
299
+
300
+ # ── runcmd ───────────────────────────────────────────────────────────────────
301
+ cat >> "$outfile" <<YAML
302
+
303
+ runcmd:
304
+ - modprobe 9p 9pnet 9pnet_virtio || true
305
+ - mkdir -p ${INSTALL_PATH}
306
+ - echo "${MOUNT_TAG} ${INSTALL_PATH} 9p trans=virtio,version=9p2000.L,ro,_netdev 0 0" >> /etc/fstab
307
+ - mount ${INSTALL_PATH} || true
308
+ - systemctl enable --now cockpit.socket
309
+ YAML
310
+
311
+ # Plugin-specific runcmd
312
+ extra_runcmd "$vm" >> "$outfile" || true
313
+
314
+ # ── footer ───────────────────────────────────────────────────────────────────
315
+ cat >> "$outfile" <<YAML
316
+
317
+ final_message: |
318
+ ${vm} VM ready.
319
+ Cockpit : https://localhost:$(cockpit_port "$vm")
320
+ SSH : ssh -p $(ssh_port "$vm") -o StrictHostKeyChecking=no test@localhost
321
+ Login : test / test
322
+ YAML
323
+ }
324
+
325
+ # ── Commands ──────────────────────────────────────────────────────────────────
326
+
327
+ cmd_download() {
328
+ local distros
329
+ read -ra distros <<< "$(resolve_distros "$@")"
330
+
331
+ for distro in "${distros[@]}"; do
332
+ local url img
333
+ case "$distro" in
334
+ arch) url="$ARCH_IMAGE_URL" ;;
335
+ debian) url="$DEBIAN_IMAGE_URL" ;;
336
+ fedora) url="$FEDORA_IMAGE_URL" ;;
337
+ *) die "No image URL configured for distro '$distro'" ;;
338
+ esac
339
+ img="$VM_DIR/$distro/base.qcow2"
340
+ mkdir -p "$VM_DIR/$distro"
341
+
342
+ if [[ -f "$img" ]]; then
343
+ info "$distro: base image already exists ($(du -sh "$img" | cut -f1)) — skipping"
344
+ continue
345
+ fi
346
+
347
+ info "$distro: downloading from $url"
348
+ wget --progress=bar:force -O "${img}.tmp" "$url"
349
+ mv "${img}.tmp" "$img"
350
+ ok "$distro: saved to $img"
351
+ done
352
+ }
353
+
354
+ cmd_build() {
355
+ info "Building $PLUGIN_NAME plugin..."
356
+ cd "$PROJECT_DIR"
357
+ npm run build
358
+ ok "Build complete → $DIST_DIR/main.js"
359
+ }
360
+
361
+ cmd_start() {
362
+ local vms
363
+ read -ra vms <<< "$(resolve_vms "$@")"
364
+
365
+ for vm in "${vms[@]}"; do
366
+ local distro sp cp vm_path bimg dimg siso udata mdata
367
+ distro="$(vm_distro "$vm")"
368
+ sp="$(ssh_port "$vm")"
369
+ cp="$(cockpit_port "$vm")"
370
+ vm_path="$VM_DIR/$vm"
371
+ bimg="$(base_img "$vm")"
372
+ dimg="$(disk_img "$vm")"
373
+ siso="$(seed_iso "$vm")"
374
+ udata="$vm_path/user-data"
375
+ mdata="$vm_path/meta-data"
376
+
377
+ [[ -f "$bimg" ]] || die "$vm: base image missing — run: $0 download $distro"
378
+ [[ -f "$DIST_DIR/main.js" ]] || die "src/main.js not found — run: $0 build (or npm run build)"
379
+
380
+ if is_running "$vm"; then
381
+ info "$vm: already running (PID $(cat "$(pid_file "$vm")"))"
382
+ continue
383
+ fi
384
+
385
+ mkdir -p "$vm_path"
386
+
387
+ if [[ ! -f "$dimg" || "$bimg" -nt "$dimg" ]]; then
388
+ info "$vm: creating overlay disk (${VM_DISK_SIZE}) from $distro base..."
389
+ qemu-img create -f qcow2 -b "$bimg" -F qcow2 "$dimg"
390
+ qemu-img resize "$dimg" "$VM_DISK_SIZE"
391
+ fi
392
+
393
+ if [[ ! -f "$siso" ]]; then
394
+ local ssh_pubkey
395
+ ssh_pubkey="$(find_ssh_pubkey)"
396
+ info "$vm: generating cloud-init seed..."
397
+ [[ -n "$ssh_pubkey" ]] && ok "Found SSH public key — injecting into VM"
398
+ generate_userdata "$vm" "$ssh_pubkey" "$udata"
399
+ printf 'instance-id: %s-01\nlocal-hostname: %s-test\n' "$vm" "$vm" > "$mdata"
400
+ make_seed_iso "$siso" "$udata" "$mdata"
401
+ fi
402
+
403
+ info "$vm: starting VM (mem=${VM_MEM}M cpus=${VM_CPUS})..."
404
+
405
+ local accel_str; accel_str="$(qemu_accel_args)"
406
+ # shellcheck disable=SC2206
407
+ local accel_args=($accel_str)
408
+
409
+ qemu-system-x86_64 \
410
+ -name "${PLUGIN_NAME}-${vm}" \
411
+ "${accel_args[@]}" \
412
+ -smp "$VM_CPUS" \
413
+ -m "$VM_MEM" \
414
+ -drive "file=${dimg},format=qcow2,if=virtio,cache=writeback" \
415
+ -drive "file=${siso},format=raw,if=virtio,readonly=on" \
416
+ -virtfs "local,path=${DIST_DIR},mount_tag=${MOUNT_TAG},security_model=none,readonly=on" \
417
+ -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:${sp}-:22,hostfwd=tcp:127.0.0.1:${cp}-:9090" \
418
+ -device virtio-net-pci,netdev=net0 \
419
+ -display none \
420
+ -serial "file:$(console_log "$vm")" \
421
+ -pidfile "$(pid_file "$vm")" \
422
+ -daemonize
423
+
424
+ ok "$vm: started (PID $(cat "$(pid_file "$vm")"))"
425
+ echo ""
426
+ echo " Cockpit → https://localhost:${cp} (accept self-signed cert)"
427
+ echo " SSH → ssh -p ${sp} -o StrictHostKeyChecking=no test@localhost"
428
+ echo " Ready? → $0 wait $vm"
429
+ echo " Logs → $0 logs $vm"
430
+ echo ""
431
+ done
432
+ }
433
+
434
+ cmd_wait() {
435
+ local vm="${1:-}"
436
+ [[ -n "$vm" ]] || die "Usage: $0 wait <vm>"
437
+ vm_index "$vm" > /dev/null
438
+ local sp; sp="$(ssh_port "$vm")"
439
+
440
+ is_running "$vm" || die "$vm is not running — start it first: $0 start $vm"
441
+
442
+ info "$vm: waiting for SSH on port $sp..."
443
+ local elapsed=0 timeout=300
444
+ while ! ssh -p "$sp" \
445
+ -o StrictHostKeyChecking=no \
446
+ -o UserKnownHostsFile=/dev/null \
447
+ -o ConnectTimeout=2 \
448
+ -o BatchMode=yes \
449
+ test@localhost true 2>/dev/null; do
450
+ sleep 5; elapsed=$((elapsed + 5))
451
+ [[ $elapsed -ge $timeout ]] && die "Timed out after ${timeout}s waiting for SSH"
452
+ printf "."
453
+ done
454
+ echo ""
455
+ info "$vm: SSH ready — waiting for cloud-init to complete..."
456
+ local ci_out
457
+ ci_out=$(ssh -p "$sp" \
458
+ -o StrictHostKeyChecking=no \
459
+ -o UserKnownHostsFile=/dev/null \
460
+ -o ConnectTimeout=5 \
461
+ -o BatchMode=yes \
462
+ test@localhost 'sudo cloud-init status --wait' 2>/dev/null || true)
463
+ if ! echo "$ci_out" | grep -q "status: done"; then
464
+ echo "WARNING: cloud-init did not reach 'done' (got: $ci_out)"
465
+ echo " Check: $0 logs $vm"
466
+ return 1
467
+ fi
468
+ echo ""
469
+ ok "$vm: VM is ready!"
470
+ echo ""
471
+ echo " Open → https://localhost:$(cockpit_port "$vm")"
472
+ echo " Login → test / test"
473
+ }
474
+
475
+ cmd_stop() {
476
+ local vms
477
+ read -ra vms <<< "$(resolve_vms "$@")"
478
+
479
+ for vm in "${vms[@]}"; do
480
+ local pf; pf="$(pid_file "$vm")"
481
+ if is_running "$vm"; then
482
+ info "$vm: stopping (PID $(cat "$pf"))..."
483
+ kill "$(cat "$pf")"
484
+ local i=0
485
+ while kill -0 "$(cat "$pf")" 2>/dev/null && [[ $i -lt 20 ]]; do
486
+ sleep 0.5; i=$((i+1))
487
+ done
488
+ rm -f "$pf"
489
+ ok "$vm: stopped"
490
+ else
491
+ info "$vm: not running"
492
+ fi
493
+ done
494
+ }
495
+
496
+ cmd_status() {
497
+ local distros=()
498
+ local seen=()
499
+ for vm in "${ALL_VMS[@]}"; do
500
+ local d; d="$(vm_distro "$vm")"
501
+ [[ " ${seen[*]} " == *" $d "* ]] && continue
502
+ seen+=("$d"); distros+=("$d")
503
+ done
504
+
505
+ echo ""
506
+ echo "Base images:"
507
+ for d in "${distros[@]}"; do
508
+ local img="$VM_DIR/$d/base.qcow2"
509
+ if [[ -f "$img" ]]; then
510
+ printf " %-8s ✓ %s\n" "$d" "$(du -sh "$img" | cut -f1)"
511
+ else
512
+ printf " %-8s ✗ not downloaded\n" "$d"
513
+ fi
514
+ done
515
+ echo ""
516
+ printf " %-18s %-8s %-8s %s\n" "VM" "STATE" "COCKPIT" "SSH"
517
+ printf " %-18s %-8s %-8s %s\n" "--" "-----" "-------" "---"
518
+ for vm in "${ALL_VMS[@]}"; do
519
+ local state cp sp
520
+ cp="$(cockpit_port "$vm")"
521
+ sp="$(ssh_port "$vm")"
522
+ if is_running "$vm"; then
523
+ state="running"
524
+ elif [[ -f "$(disk_img "$vm")" ]]; then
525
+ state="stopped"
526
+ else
527
+ state="not created"
528
+ fi
529
+ printf " %-18s %-8s :%-7s :%s\n" "$vm" "$state" "$cp" "$sp"
530
+ done
531
+ echo ""
532
+ }
533
+
534
+ cmd_ssh() {
535
+ local vm="${1:-}"
536
+ [[ -n "$vm" ]] || die "Usage: $0 ssh <vm>"
537
+ vm_index "$vm" > /dev/null
538
+ is_running "$vm" || die "$vm is not running — start it: $0 start $vm"
539
+ exec ssh \
540
+ -p "$(ssh_port "$vm")" \
541
+ -o StrictHostKeyChecking=no \
542
+ -o UserKnownHostsFile=/dev/null \
543
+ test@localhost
544
+ }
545
+
546
+ cmd_logs() {
547
+ local vm="${1:-}"
548
+ [[ -n "$vm" ]] || die "Usage: $0 logs <vm>"
549
+ vm_index "$vm" > /dev/null
550
+ local log; log="$(console_log "$vm")"
551
+ [[ -f "$log" ]] || die "No console log yet for $vm (start it first)"
552
+ exec tail -f "$log"
553
+ }
554
+
555
+ cmd_clean() {
556
+ local vms
557
+ read -ra vms <<< "$(resolve_vms "$@")"
558
+ for vm in "${vms[@]}"; do
559
+ is_running "$vm" && { info "$vm: stopping first"; cmd_stop "$vm"; }
560
+ info "$vm: removing disk and cloud-init state (base image kept)..."
561
+ rm -f "$(disk_img "$vm")" "$(seed_iso "$vm")" \
562
+ "$VM_DIR/$vm/user-data" "$VM_DIR/$vm/meta-data" \
563
+ "$(console_log "$vm")" "$(pid_file "$vm")"
564
+ ok "$vm: cleaned — next 'start' will reprovision from the base image"
565
+ done
566
+ }
567
+
568
+ cmd_rebuild() {
569
+ local vms
570
+ read -ra vms <<< "$(resolve_vms "$@")"
571
+ cmd_clean "${vms[@]}"
572
+ cmd_start "${vms[@]}"
573
+ }
574
+
575
+ cmd_reset() {
576
+ local distros
577
+ read -ra distros <<< "$(resolve_distros "$@")"
578
+ for distro in "${distros[@]}"; do
579
+ for vm in "${ALL_VMS[@]}"; do
580
+ [[ "$(vm_distro "$vm")" == "$distro" ]] && is_running "$vm" && cmd_stop "$vm"
581
+ done
582
+ info "$distro: removing all VM files including base image..."
583
+ rm -rf "$VM_DIR/$distro"
584
+ for vm in "${ALL_VMS[@]}"; do
585
+ [[ "$(vm_distro "$vm")" == "$distro" ]] && rm -rf "$VM_DIR/$vm"
586
+ done
587
+ ok "$distro: reset — run 'download $distro' to start fresh"
588
+ done
589
+ }
590
+
591
+ # ── Main ──────────────────────────────────────────────────────────────────────
592
+
593
+ check_deps
594
+
595
+ case "${1:-}" in
596
+ download) shift; cmd_download "$@" ;;
597
+ build) shift; cmd_build ;;
598
+ start) shift; cmd_start "$@" ;;
599
+ wait) shift; cmd_wait "$@" ;;
600
+ stop) shift; cmd_stop "$@" ;;
601
+ status) cmd_status ;;
602
+ ssh) shift; cmd_ssh "$@" ;;
603
+ logs) shift; cmd_logs "$@" ;;
604
+ clean) shift; cmd_clean "$@" ;;
605
+ rebuild) shift; cmd_rebuild "$@" ;;
606
+ reset) shift; cmd_reset "$@" ;;
607
+ *) usage ;;
608
+ esac
@@ -0,0 +1,7 @@
1
+ import { ComponentType } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ export function bootstrapPlugin(App: ComponentType): void {
5
+ const root = createRoot(document.getElementById("root")!);
6
+ root.render(<App />);
7
+ }
@@ -0,0 +1,63 @@
1
+ // Ambient type declarations for the Cockpit browser global.
2
+ // Superset covering cockpit-caddy and cockpit-compose usage patterns.
3
+ /// <reference path="./css.d.ts" />
4
+
5
+ declare interface CockpitProcess extends Promise<string> {
6
+ stream(callback: (data: string) => void): CockpitProcess;
7
+ close(problem?: string): void;
8
+ input(data?: string, stream?: boolean): void;
9
+ wait?(): Promise<string>;
10
+ }
11
+
12
+ declare interface CockpitChannel {
13
+ close(): void;
14
+ send(data: string): void;
15
+ addEventListener(event: "message", callback: (event: Event, payload: string) => void): void;
16
+ addEventListener(event: "close", callback: (event: Event, options: { problem?: string; message?: string }) => void): void;
17
+ }
18
+
19
+ declare interface CockpitHttpRequestOptions {
20
+ method: string;
21
+ path: string;
22
+ headers?: Record<string, string>;
23
+ body?: string;
24
+ }
25
+
26
+ declare interface CockpitHttpClient {
27
+ get(path: string, params?: Record<string, string>): Promise<string>;
28
+ post(path: string, body: string, headers?: Record<string, string>): Promise<string>;
29
+ request(options: CockpitHttpRequestOptions): Promise<{ status: number; headers: Record<string, string>; data: string }>;
30
+ close(): void;
31
+ }
32
+
33
+ declare interface CockpitUser {
34
+ id: number;
35
+ name: string;
36
+ home: string;
37
+ shell: string;
38
+ groups: string[];
39
+ }
40
+
41
+ declare const cockpit: {
42
+ user(): Promise<CockpitUser>;
43
+ spawn(
44
+ args: string[],
45
+ options?: { superuser?: "try" | "require"; err?: string; environ?: string[] }
46
+ ): CockpitProcess;
47
+ http(options: { port?: number; address?: string }): CockpitHttpClient;
48
+ file(
49
+ path: string,
50
+ options?: { superuser?: "try" | "require"; syntax?: unknown }
51
+ ): {
52
+ read(): Promise<string>;
53
+ replace(content: string): Promise<void>;
54
+ watch(callback: (content: string | null) => void): CockpitChannel;
55
+ };
56
+ channel(options: {
57
+ payload: string;
58
+ spawn?: string[];
59
+ pty?: boolean;
60
+ superuser?: "try" | "require";
61
+ [key: string]: unknown;
62
+ }): CockpitChannel;
63
+ };