@aiassesstech/mighty-mark 0.3.31 → 0.3.33

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.
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env bash
2
+ # ═══════════════════════════════════════════════════════════════════
3
+ # nole-fleet-backup — Restore from a backup archive
4
+ #
5
+ # Supports both full and light tiered backups, and archives that
6
+ # were split into chunks for GitHub storage.
7
+ #
8
+ # Usage:
9
+ # ./fleet-restore.sh # List all
10
+ # ./fleet-restore.sh --list # List all
11
+ # ./fleet-restore.sh --inspect fleet-full-2026-03-02-090000 # Show contents
12
+ # ./fleet-restore.sh --verify fleet-full-2026-03-02-090000 # Verify checksum
13
+ # ./fleet-restore.sh --extract fleet-full-2026-03-02-090000 # Extract to /tmp
14
+ # ./fleet-restore.sh --restore fleet-full-2026-03-02-090000 # Full restore
15
+ # ./fleet-restore.sh --from-github # Download + restore from GitHub
16
+ # ═══════════════════════════════════════════════════════════════════
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+
22
+ if [[ -f "$SCRIPT_DIR/config.local.sh" ]]; then
23
+ source "$SCRIPT_DIR/config.local.sh"
24
+ else
25
+ source "$SCRIPT_DIR/config.sh"
26
+ fi
27
+
28
+ RED='\033[0;31m'
29
+ GREEN='\033[0;32m'
30
+ YELLOW='\033[1;33m'
31
+ NC='\033[0m'
32
+
33
+ log() { echo -e "${GREEN}[restore]${NC} $1"; }
34
+ warn() { echo -e "${YELLOW}[restore]${NC} $1"; }
35
+ err() { echo -e "${RED}[restore]${NC} $1" >&2; }
36
+
37
+ list_backups() {
38
+ echo "Available backups in $BACKUP_DIR:"
39
+ echo ""
40
+ if [[ ! -d "$BACKUP_DIR" ]]; then
41
+ err "Backup directory not found: $BACKUP_DIR"
42
+ exit 1
43
+ fi
44
+
45
+ local count=0
46
+ for archive in $(find "$BACKUP_DIR" -name 'fleet-*.tar.gz' | sort -r); do
47
+ local name=$(basename "$archive" .tar.gz)
48
+ local size=$(stat -c%s "$archive" 2>/dev/null || stat -f%z "$archive" 2>/dev/null || echo 0)
49
+ local size_mb=$(awk "BEGIN {printf \"%.1f\", $size / 1048576}")
50
+
51
+ local tier="unknown"
52
+ if [[ "$name" == *"-full-"* ]]; then
53
+ tier="FULL"
54
+ elif [[ "$name" == *"-light-"* ]]; then
55
+ tier="light"
56
+ elif [[ "$name" == *"fleet-backup-"* ]]; then
57
+ tier="legacy"
58
+ fi
59
+
60
+ printf " %-8s %-42s %8s MB\n" "[$tier]" "$name" "$size_mb"
61
+ count=$((count + 1))
62
+ done
63
+
64
+ echo ""
65
+ echo "$count backup(s) found. Retention: $RETENTION_DAYS days."
66
+ }
67
+
68
+ get_archive_path() {
69
+ local id="$1"
70
+ local path="$BACKUP_DIR/${id}.tar.gz"
71
+
72
+ if [[ -f "$path" ]]; then
73
+ echo "$path"
74
+ return
75
+ fi
76
+
77
+ # Try legacy naming (fleet-backup-YYYY-MM-DD.tar.gz)
78
+ local legacy="$BACKUP_DIR/fleet-backup-${id}.tar.gz"
79
+ if [[ -f "$legacy" ]]; then
80
+ echo "$legacy"
81
+ return
82
+ fi
83
+
84
+ # Search for partial match
85
+ local matches
86
+ matches=$(find "$BACKUP_DIR" -name "*${id}*.tar.gz" 2>/dev/null | head -1)
87
+ if [[ -n "$matches" ]]; then
88
+ echo "$matches"
89
+ return
90
+ fi
91
+
92
+ err "No backup found matching: $id"
93
+ err "Expected: $path"
94
+ echo ""
95
+ list_backups
96
+ exit 1
97
+ }
98
+
99
+ verify_backup() {
100
+ local id="$1"
101
+ local archive
102
+ archive=$(get_archive_path "$id")
103
+
104
+ log "Verifying checksum for $(basename "$archive")..."
105
+
106
+ local actual
107
+ actual=$(sha256sum "$archive" 2>/dev/null | awk '{print $1}' || shasum -a 256 "$archive" | awk '{print $1}')
108
+
109
+ if [[ -f "$MANIFEST_FILE" ]]; then
110
+ local manifest_checksum
111
+ manifest_checksum=$(grep -o '"checksum": *"[^"]*"' "$MANIFEST_FILE" | head -1 | sed 's/.*: *"sha256://;s/"//')
112
+ local manifest_archive
113
+ manifest_archive=$(grep -o '"archive": *"[^"]*"' "$MANIFEST_FILE" | head -1 | sed 's/.*: *"//;s/"//')
114
+
115
+ if [[ "$(basename "$archive")" == "$manifest_archive" && -n "$manifest_checksum" ]]; then
116
+ if [[ "$actual" == "$manifest_checksum" ]]; then
117
+ log "Checksum VERIFIED: sha256:$actual"
118
+ return 0
119
+ else
120
+ err "Checksum MISMATCH!"
121
+ err " Expected: $manifest_checksum"
122
+ err " Actual: $actual"
123
+ return 1
124
+ fi
125
+ fi
126
+ fi
127
+
128
+ log "Checksum: sha256:$actual (no matching manifest to compare against)"
129
+ }
130
+
131
+ inspect_backup() {
132
+ local id="$1"
133
+ local archive
134
+ archive=$(get_archive_path "$id")
135
+
136
+ log "Contents of $(basename "$archive"):"
137
+ echo ""
138
+ tar -tzf "$archive" | head -100
139
+ echo ""
140
+
141
+ local total
142
+ total=$(tar -tzf "$archive" | wc -l | tr -d ' ')
143
+ log "Total entries: $total"
144
+ }
145
+
146
+ extract_backup() {
147
+ local id="$1"
148
+ local archive
149
+ archive=$(get_archive_path "$id")
150
+ local name=$(basename "$archive" .tar.gz)
151
+ local extract_dir="/tmp/fleet-restore-${name}"
152
+
153
+ if [[ -d "$extract_dir" ]]; then
154
+ warn "Extract directory already exists: $extract_dir"
155
+ read -p "Remove and re-extract? (y/N): " confirm
156
+ [[ "$confirm" =~ ^[Yy]$ ]] || exit 0
157
+ rm -rf "$extract_dir"
158
+ fi
159
+
160
+ log "Extracting to $extract_dir..."
161
+ mkdir -p "$extract_dir"
162
+ tar -xzf "$archive" -C "$extract_dir"
163
+
164
+ log "Extracted. Review contents:"
165
+ echo ""
166
+ ls -la "$extract_dir/fleet-backup/"
167
+ echo ""
168
+ log "To inspect: ls $extract_dir/fleet-backup/"
169
+ log "To clean up: rm -rf $extract_dir"
170
+ }
171
+
172
+ from_github() {
173
+ if [[ ! -d "${GITHUB_REPO_DIR:-}" ]]; then
174
+ err "GITHUB_REPO_DIR not set or does not exist: ${GITHUB_REPO_DIR:-<unset>}"
175
+ exit 1
176
+ fi
177
+
178
+ log "Fetching latest backup from GitHub..."
179
+ local work_dir
180
+ work_dir=$(mktemp -d "/tmp/fleet-github-restore-XXXXXX")
181
+
182
+ (
183
+ cd "$GITHUB_REPO_DIR"
184
+ git fetch origin "${GITHUB_BACKUP_BRANCH}" 2>/dev/null || {
185
+ err "Could not fetch branch: $GITHUB_BACKUP_BRANCH"
186
+ exit 1
187
+ }
188
+
189
+ git worktree add "$work_dir" "origin/$GITHUB_BACKUP_BRANCH" 2>/dev/null || {
190
+ # Fallback: archive from remote
191
+ git archive "origin/$GITHUB_BACKUP_BRANCH" | tar -x -C "$work_dir"
192
+ }
193
+ )
194
+
195
+ cd "$work_dir"
196
+
197
+ # Check for chunked archive and reassemble
198
+ local chunks
199
+ chunks=$(find . -name 'fleet-*.tar.gz.part-*' 2>/dev/null | sort)
200
+ if [[ -n "$chunks" ]]; then
201
+ local base_name
202
+ base_name=$(echo "$chunks" | head -1 | sed 's/\.part-[0-9]*//')
203
+ base_name=$(basename "$base_name")
204
+ log "Reassembling ${base_name} from $(echo "$chunks" | wc -l | tr -d ' ') chunks..."
205
+ cat $chunks > "$BACKUP_DIR/$base_name"
206
+ log "Reassembled: $BACKUP_DIR/$base_name"
207
+ else
208
+ local archive
209
+ archive=$(find . -name 'fleet-*.tar.gz' 2>/dev/null | head -1)
210
+ if [[ -n "$archive" ]]; then
211
+ cp "$archive" "$BACKUP_DIR/"
212
+ log "Copied: $(basename "$archive") to $BACKUP_DIR/"
213
+ else
214
+ err "No archive found on the backups branch"
215
+ rm -rf "$work_dir"
216
+ exit 1
217
+ fi
218
+ fi
219
+
220
+ # Copy manifest
221
+ [[ -f "latest-manifest.json" ]] && cp "latest-manifest.json" "$MANIFEST_FILE"
222
+
223
+ rm -rf "$work_dir"
224
+ git -C "$GITHUB_REPO_DIR" worktree prune 2>/dev/null || true
225
+
226
+ log "Archive downloaded from GitHub. Use --list to see it, --restore to apply."
227
+ }
228
+
229
+ restore_backup() {
230
+ local id="$1"
231
+ local archive
232
+ archive=$(get_archive_path "$id")
233
+ local name=$(basename "$archive" .tar.gz)
234
+
235
+ local tier="unknown"
236
+ if [[ "$name" == *"-full-"* ]]; then
237
+ tier="FULL"
238
+ elif [[ "$name" == *"-light-"* ]]; then
239
+ tier="LIGHT"
240
+ fi
241
+
242
+ echo ""
243
+ warn "═══════════════════════════════════════════════════════"
244
+ warn " $tier RESTORE from backup: $name"
245
+ warn "═══════════════════════════════════════════════════════"
246
+ echo ""
247
+
248
+ if [[ "$tier" == "LIGHT" ]]; then
249
+ warn "This is a LIGHT backup. It contains only:"
250
+ echo " - Agent .md files (identity, prompts, memory markdown)"
251
+ echo " - Sanitized openclaw.json configs"
252
+ echo " - Systemd/cron/logrotate configs"
253
+ echo ""
254
+ warn "It does NOT contain:"
255
+ echo " - Extension dist/ code"
256
+ echo " - Vector indexes (.sqlite)"
257
+ echo " - Data stores (grillo-data, .noah-data, etc.)"
258
+ echo " - Session transcripts"
259
+ echo " - Fleet-bus workspace"
260
+ echo ""
261
+ warn "For a complete restore, use the latest FULL backup first,"
262
+ warn "then overlay a LIGHT backup for the most recent .md state."
263
+ else
264
+ warn "This will overwrite the following on this system:"
265
+ echo " - $OPENCLAW_HOME/extensions/*/dist/"
266
+ echo " - $OPENCLAW_HOME/extensions/*/agent/"
267
+ echo " - $OPENCLAW_HOME/agents/"
268
+ echo " - Agent data stores (grillo-data, .noah-data, etc.)"
269
+ echo " - Fleet-bus workspace"
270
+ echo " - Mighty Mark watchdog scripts"
271
+ echo " - Systemd/cron configs"
272
+ fi
273
+ echo ""
274
+ warn "Secrets (.env, credentials, API keys) are NOT in the"
275
+ warn "backup and will NOT be affected."
276
+ echo ""
277
+
278
+ read -p "Stop openclaw-gateway before restore? (Y/n): " stop_gw
279
+ if [[ ! "$stop_gw" =~ ^[Nn]$ ]]; then
280
+ log "Stopping openclaw-gateway..."
281
+ systemctl stop openclaw-gateway 2>/dev/null || warn "Could not stop gateway (may not be running)"
282
+ fi
283
+
284
+ verify_backup "$id" || {
285
+ err "Checksum verification failed. Aborting restore."
286
+ exit 1
287
+ }
288
+
289
+ local extract_dir="/tmp/fleet-restore-${name}"
290
+ rm -rf "$extract_dir"
291
+ mkdir -p "$extract_dir"
292
+ tar -xzf "$archive" -C "$extract_dir"
293
+ local src="$extract_dir/fleet-backup"
294
+
295
+ read -p "Proceed with restore? Type YES to confirm: " confirm
296
+ if [[ "$confirm" != "YES" ]]; then
297
+ err "Restore cancelled."
298
+ rm -rf "$extract_dir"
299
+ exit 1
300
+ fi
301
+
302
+ # Restore extensions
303
+ if [[ -d "$src/extensions" ]]; then
304
+ log "Restoring extensions..."
305
+ for agent_dir in "$src/extensions"/*/; do
306
+ agent_name=$(basename "$agent_dir")
307
+ target="$OPENCLAW_HOME/extensions/$agent_name"
308
+ if [[ -d "$target" ]]; then
309
+ [[ -d "$agent_dir/dist" ]] && cp -r "$agent_dir/dist"/* "$target/dist/" 2>/dev/null || true
310
+ [[ -d "$agent_dir/agent" ]] && cp -r "$agent_dir/agent"/* "$target/agent/" 2>/dev/null || true
311
+ [[ -f "$agent_dir/package.json" ]] && cp "$agent_dir/package.json" "$target/"
312
+ log " Restored: $agent_name"
313
+ else
314
+ warn " Skipped $agent_name (target dir does not exist)"
315
+ fi
316
+ done
317
+ fi
318
+
319
+ # Restore agent workspaces
320
+ if [[ -d "$src/agents" ]]; then
321
+ log "Restoring agent workspaces..."
322
+ cp -r "$src/agents"/* "$OPENCLAW_HOME/agents/" 2>/dev/null || true
323
+ fi
324
+
325
+ # Full-only restores
326
+ if [[ "$tier" == "FULL" ]] || [[ "$tier" == "unknown" ]]; then
327
+
328
+ # Restore vector indexes
329
+ if [[ -d "$src/memory" ]]; then
330
+ log "Restoring vector indexes..."
331
+ mkdir -p "$OPENCLAW_HOME/memory"
332
+ cp "$src/memory"/*.sqlite "$OPENCLAW_HOME/memory/" 2>/dev/null || true
333
+ fi
334
+
335
+ # Restore data stores
336
+ for data_dir in grillo-data .noah-data nole-data .mark-data .fleet-data .sam-artifacts; do
337
+ if [[ -d "$src/$data_dir" ]]; then
338
+ log "Restoring data store: $data_dir"
339
+ mkdir -p "$OPENCLAW_HOME/$data_dir"
340
+ cp -r "$src/$data_dir"/* "$OPENCLAW_HOME/$data_dir/" 2>/dev/null || true
341
+ fi
342
+ done
343
+
344
+ # Restore workspace
345
+ if [[ -d "$src/workspace" ]]; then
346
+ log "Restoring workspace (fleet-bus, skills)..."
347
+ mkdir -p "$OPENCLAW_HOME/workspace"
348
+ cp -r "$src/workspace"/* "$OPENCLAW_HOME/workspace/" 2>/dev/null || true
349
+ fi
350
+
351
+ # Restore watchdog
352
+ if [[ -d "$src/mighty-mark-watchdog" ]]; then
353
+ log "Restoring Mighty Mark watchdog scripts..."
354
+ cp -r "$src/mighty-mark-watchdog"/* "$MIGHTY_MARK_OPT/" 2>/dev/null || true
355
+ fi
356
+
357
+ fi
358
+
359
+ # Restore system configs
360
+ if [[ -d "$src/system" ]]; then
361
+ log "Restoring system configs..."
362
+ for f in "$src/system"/*; do
363
+ fname=$(basename "$f")
364
+ case "$fname" in
365
+ *.service) cp "$f" /etc/systemd/system/ && log " Restored: $fname" ;;
366
+ mighty-mark|fleet-backup) cp "$f" /etc/cron.d/ && log " Restored cron: $fname" ;;
367
+ *) log " Skipped system file: $fname" ;;
368
+ esac
369
+ done
370
+ systemctl daemon-reload 2>/dev/null || true
371
+ fi
372
+
373
+ rm -rf "$extract_dir"
374
+
375
+ echo ""
376
+ log "Restore complete from backup: $name [$tier]"
377
+ echo ""
378
+ warn "IMPORTANT — Manual steps remaining:"
379
+ echo " 1. Verify openclaw.json has correct API keys (not in backup)"
380
+ echo " 2. Verify /opt/mighty-mark/.env has Telegram credentials"
381
+ echo " 3. Verify ~/.nole/credentials if Nole wallet is configured"
382
+ echo " 4. Re-install fleet-bus for each extension (npm pack pattern)"
383
+ echo " 5. Restart gateway: systemctl start openclaw-gateway"
384
+ echo " 6. Verify logs: journalctl -u openclaw-gateway --no-pager -n 50"
385
+ }
386
+
387
+ # ─── CLI ──────────────────────────────────────────────────────────
388
+
389
+ case "${1:---list}" in
390
+ --list|-l)
391
+ list_backups
392
+ ;;
393
+ --inspect|-i)
394
+ [[ -z "${2:-}" ]] && { err "Usage: $0 --inspect ARCHIVE-NAME"; exit 1; }
395
+ inspect_backup "$2"
396
+ ;;
397
+ --verify|-v)
398
+ [[ -z "${2:-}" ]] && { err "Usage: $0 --verify ARCHIVE-NAME"; exit 1; }
399
+ verify_backup "$2"
400
+ ;;
401
+ --extract|-e)
402
+ [[ -z "${2:-}" ]] && { err "Usage: $0 --extract ARCHIVE-NAME"; exit 1; }
403
+ extract_backup "$2"
404
+ ;;
405
+ --restore|-r)
406
+ [[ -z "${2:-}" ]] && { err "Usage: $0 --restore ARCHIVE-NAME"; exit 1; }
407
+ restore_backup "$2"
408
+ ;;
409
+ --from-github|-g)
410
+ from_github
411
+ ;;
412
+ --help|-h)
413
+ echo "Usage: $0 [command] [archive-name]"
414
+ echo ""
415
+ echo "Commands:"
416
+ echo " --list, -l List available backups (default)"
417
+ echo " --inspect, -i ARCHIVE-NAME Show archive contents"
418
+ echo " --verify, -v ARCHIVE-NAME Verify checksum against manifest"
419
+ echo " --extract, -e ARCHIVE-NAME Extract to /tmp for review"
420
+ echo " --restore, -r ARCHIVE-NAME Restore (interactive, with confirmation)"
421
+ echo " --from-github, -g Download latest backup from GitHub"
422
+ echo " --help, -h Show this help"
423
+ echo ""
424
+ echo "Archive name: e.g. fleet-full-2026-03-02-090000"
425
+ echo " (omit .tar.gz extension)"
426
+ echo ""
427
+ echo "Recommended disaster recovery:"
428
+ echo " 1. $0 --from-github # download from GitHub"
429
+ echo " 2. $0 --restore fleet-full-YYYY-MM-DD-HHMMSS # restore latest full"
430
+ echo " 3. $0 --restore fleet-light-YYYY-MM-DD-HHMMSS # overlay latest light"
431
+ ;;
432
+ *)
433
+ err "Unknown command: $1"
434
+ err "Run $0 --help for usage"
435
+ exit 1
436
+ ;;
437
+ esac