@inoku/lepan 0.0.1 → 0.7.59

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,831 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ RELEASE_BASE="${LIFEDRIVE_RELEASE_BASE:-https://github.com/cfa532/lifedrive-installer/releases/latest/download}"
5
+ ARCHIVE_NAME="lifedrive-bundle.tar.gz"
6
+ CHECKSUM_NAME="$ARCHIVE_NAME.sha256"
7
+ LEITHER_WORKDIR="${LIFEDRIVE_WORKDIR:-}"
8
+ UPGRADE_ONLY=0
9
+ MANAGEMENT_ONLY=0
10
+ INSTALL_LEITHER=1
11
+ LEITHER_SERVICE_ONLY=0
12
+ LEITHER_SERVICE_BOOTSTRAPPED=0
13
+ HOUSEHOLD_CONFIG=""
14
+ HOUSEHOLD_SETUP=0
15
+ STORAGE_MAX_GB="${LIFEDRIVE_STORAGE_MAX_GB:-}"
16
+ setup_command_args=()
17
+ setup_arg_count=0
18
+ operation_count=0
19
+
20
+ usage() {
21
+ cat <<'EOF'
22
+ Usage: lifedrive-install.sh [installer options] [setup options]
23
+
24
+ Installer options:
25
+ --leither-root DIR Select an existing node or the directory for a new Leither installation.
26
+ --no-install-leither Require a running Leither node; do not install or start one.
27
+ --leither-service Set up Leither boot startup only; do not install or change LePan.
28
+ --storage-max-gb GB Maximum disk space for a newly installed Leither node (default: 100 GB).
29
+ --release-base URL Alternate GitHub Release asset base URL.
30
+ --upgrade Upgrade an existing LePan without changing device authorization.
31
+ --household Prepare mobile household setup using the existing node address.
32
+ --household-config FILE Install mobile-led household setup using a private service configuration.
33
+
34
+ Setup options are forwarded to lifeDrive-setup.sh. Common examples:
35
+ --registry-url URL
36
+ --publisher-key FILE
37
+ --add-device LABEL
38
+ --identity-out FILE
39
+ --list-devices
40
+ --revoke-device UID
41
+ --skip-domain
42
+ EOF
43
+ }
44
+
45
+ while (( $# )); do
46
+ case "$1" in
47
+ --leither-service)
48
+ LEITHER_SERVICE_ONLY=1
49
+ ;;
50
+ --no-install-leither)
51
+ INSTALL_LEITHER=0
52
+ ;;
53
+ --household)
54
+ HOUSEHOLD_SETUP=1
55
+ ;;
56
+ --household-config)
57
+ shift
58
+ [[ $# -gt 0 && -f "$1" ]] || { echo "--household-config requires a configuration file" >&2; exit 2; }
59
+ HOUSEHOLD_CONFIG="$1"
60
+ ;;
61
+ --leither-root)
62
+ shift
63
+ [[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--leither-root requires a directory" >&2; exit 2; }
64
+ LEITHER_WORKDIR="$1"
65
+ ;;
66
+ --storage-max-gb)
67
+ shift
68
+ [[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--storage-max-gb requires a whole number of gigabytes" >&2; exit 2; }
69
+ STORAGE_MAX_GB="$1"
70
+ ;;
71
+ --release-base)
72
+ shift
73
+ [[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "--release-base requires a URL" >&2; exit 2; }
74
+ RELEASE_BASE="${1%/}"
75
+ ;;
76
+ --upgrade)
77
+ UPGRADE_ONLY=1
78
+ operation_count=$((operation_count + 1))
79
+ setup_command_args+=("--upgrade")
80
+ setup_arg_count=$((setup_arg_count + 1))
81
+ ;;
82
+ --list-devices)
83
+ MANAGEMENT_ONLY=1
84
+ operation_count=$((operation_count + 1))
85
+ setup_command_args+=("$1")
86
+ setup_arg_count=$((setup_arg_count + 1))
87
+ ;;
88
+ --add-device|--revoke-device|--registry-url|--publisher-key|--identity-out)
89
+ option="$1"
90
+ shift
91
+ [[ $# -gt 0 && -n "$1" && "$1" != --* ]] || { echo "$option requires a value" >&2; exit 2; }
92
+ if [[ "$option" == --add-device || "$option" == --revoke-device ]]; then
93
+ MANAGEMENT_ONLY=1
94
+ operation_count=$((operation_count + 1))
95
+ fi
96
+ setup_command_args+=("$option" "$1")
97
+ setup_arg_count=$((setup_arg_count + 2))
98
+ ;;
99
+ --skip-domain) setup_command_args+=("$1"); setup_arg_count=$((setup_arg_count + 1)) ;;
100
+ -h|--help) usage; exit 0 ;;
101
+ *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
102
+ esac
103
+ shift
104
+ done
105
+
106
+ if (( operation_count > 1 )); then
107
+ echo "Choose only one of --upgrade, --add-device, --list-devices, or --revoke-device." >&2
108
+ exit 2
109
+ fi
110
+ if (( LEITHER_SERVICE_ONLY )) && { (( ! INSTALL_LEITHER || operation_count || HOUSEHOLD_SETUP || setup_arg_count )) || [[ -n "$HOUSEHOLD_CONFIG" || -n "$STORAGE_MAX_GB" ]]; }; then
111
+ echo "Use --leither-service with only --leither-root and installer source options." >&2
112
+ exit 2
113
+ fi
114
+ if (( HOUSEHOLD_SETUP )) && { [[ -n "$HOUSEHOLD_CONFIG" ]] || (( MANAGEMENT_ONLY )); }; then
115
+ echo "Use --household separately from --household-config and device-management options." >&2
116
+ exit 2
117
+ fi
118
+ valid_storage_gb() {
119
+ local value="$1"
120
+ [[ "$value" =~ ^[1-9][0-9]*$ ]] || return 1
121
+ (( ${#value} < 11 )) && return 0
122
+ (( ${#value} == 11 )) && (( 10#$value <= 18446744073 ))
123
+ }
124
+ if [[ -n "$STORAGE_MAX_GB" ]] && ! valid_storage_gb "$STORAGE_MAX_GB"; then
125
+ echo "Leither storage must be a positive whole number of gigabytes." >&2
126
+ exit 2
127
+ fi
128
+
129
+ for required in node curl tar; do
130
+ command -v "$required" >/dev/null 2>&1 || { echo "LePan installation requires $required." >&2; exit 1; }
131
+ done
132
+
133
+ find_leither_pids() {
134
+ ps -axww -o pid=,comm= 2>/dev/null | awk '
135
+ { pid = $1; sub(/^[[:space:]]*[0-9]+[[:space:]]+/, "") }
136
+ $0 == "Leither" || $0 ~ /\/Leither$/ { print pid }
137
+ '
138
+ }
139
+
140
+ leither_version() {
141
+ local version_json
142
+ version_json=$(cd "$1" && ./Leither version --json) || return 1
143
+ node -e '
144
+ try {
145
+ const reply = JSON.parse(process.argv[1]);
146
+ const reported = reply?.data?.version ?? reply?.version;
147
+ if (typeof reported !== "string") throw new Error("missing version");
148
+ const version = reported.replace(/^V/, "");
149
+ if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error("unknown version");
150
+ const [major, minor, patch] = version.split(".").map(Number);
151
+ if (major === 0 && (minor < 24 || (minor === 24 && patch < 11))) throw new Error("too old");
152
+ process.stdout.write(version);
153
+ } catch {
154
+ console.error("LePan requires Leither V0.24.11 or newer. Upgrade Leither separately; existing nodes are never overwritten.");
155
+ process.exit(1);
156
+ }
157
+ ' "$version_json"
158
+ }
159
+
160
+ available_storage_gb() {
161
+ # POSIX df reports 1024-byte blocks with -k. The result is informational and
162
+ # also prevents a fresh node from receiving a quota the selected disk cannot
163
+ # currently hold. Failure to inspect the filesystem does not invent a value.
164
+ LC_ALL=C df -Pk "$LEITHER_WORKDIR" 2>/dev/null | awk 'NR == 2 && $4 ~ /^[0-9]+$/ { print int(($4 * 1024) / 1000000000) }'
165
+ }
166
+
167
+ configure_new_node_storage() {
168
+ local available_gb selected_gb default_gb=100 config
169
+ config="$LEITHER_WORKDIR/ds/config"
170
+ [[ -f "$config" && ! -L "$config" ]] || {
171
+ echo "Leither initialization did not create a regular datastore configuration at $config." >&2
172
+ return 1
173
+ }
174
+ available_gb=$(available_storage_gb)
175
+ echo
176
+ echo "Choose the maximum hard-drive space Leither may use for LePan and any other data on this node."
177
+ if [[ "$available_gb" =~ ^[0-9]+$ ]]; then
178
+ echo "Available now on the selected drive: ${available_gb} GB."
179
+ fi
180
+ if [[ -n "$STORAGE_MAX_GB" ]]; then
181
+ selected_gb="$STORAGE_MAX_GB"
182
+ echo "Leither storage limit: ${selected_gb} GB."
183
+ elif [[ -t 0 ]]; then
184
+ while true; do
185
+ read -r -p "Maximum Leither storage in GB [$default_gb]: " selected_gb
186
+ selected_gb="${selected_gb:-$default_gb}"
187
+ if ! valid_storage_gb "$selected_gb"; then
188
+ echo "Enter a positive whole number of gigabytes." >&2
189
+ continue
190
+ fi
191
+ if [[ "$available_gb" =~ ^[0-9]+$ ]] && (( selected_gb > available_gb )); then
192
+ echo "That exceeds the ${available_gb} GB currently available on this drive. Choose a smaller limit." >&2
193
+ continue
194
+ fi
195
+ break
196
+ done
197
+ else
198
+ selected_gb="$default_gb"
199
+ echo "No interactive terminal was detected; using the ${default_gb} GB default."
200
+ fi
201
+ if [[ "$available_gb" =~ ^[0-9]+$ ]] && (( selected_gb > available_gb )); then
202
+ echo "The selected limit (${selected_gb} GB) exceeds the ${available_gb} GB currently available on this drive." >&2
203
+ echo "Free space or rerun setup with --storage-max-gb set to a smaller value." >&2
204
+ return 1
205
+ fi
206
+ node - "$config" "${selected_gb}GB" <<'NODE'
207
+ const fs = require("node:fs"), path = require("node:path");
208
+ const file = process.argv[2], desired = process.argv[3];
209
+ const info = fs.lstatSync(file);
210
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error("Leither datastore configuration is not a regular file");
211
+ const source = fs.readFileSync(file, "utf8");
212
+ const parsed = JSON.parse(source);
213
+ const current = parsed?.Datastore?.StorageMax;
214
+ if (typeof current !== "string") throw new Error("Leither datastore StorageMax is not a string");
215
+ const key = '"StorageMax"';
216
+ const keyIndex = source.indexOf(key);
217
+ if (keyIndex < 0 || source.indexOf(key, keyIndex + key.length) >= 0) throw new Error("Leither datastore StorageMax must occur exactly once");
218
+ const currentJSON = JSON.stringify(current), nextJSON = JSON.stringify(desired);
219
+ const valueIndex = source.indexOf(currentJSON, keyIndex + key.length);
220
+ if (valueIndex < 0) throw new Error("Leither datastore StorageMax value could not be located");
221
+ const next = source.slice(0, valueIndex) + nextJSON + source.slice(valueIndex + currentJSON.length);
222
+ const verified = JSON.parse(next);
223
+ if (verified?.Datastore?.StorageMax !== desired) throw new Error("updated Leither datastore configuration did not validate");
224
+ const temporary = `${file}.lifedrive-install-${process.pid}`;
225
+ let descriptor;
226
+ try {
227
+ descriptor = fs.openSync(temporary, "wx", info.mode & 0o777);
228
+ fs.writeFileSync(descriptor, next, "utf8");
229
+ fs.fsyncSync(descriptor);
230
+ fs.closeSync(descriptor);
231
+ descriptor = undefined;
232
+ fs.renameSync(temporary, file);
233
+ try {
234
+ const directory = fs.openSync(path.dirname(file), "r");
235
+ fs.fsyncSync(directory);
236
+ fs.closeSync(directory);
237
+ } catch {}
238
+ } catch (error) {
239
+ if (descriptor !== undefined) fs.closeSync(descriptor);
240
+ try { fs.unlinkSync(temporary); } catch {}
241
+ throw error;
242
+ }
243
+ NODE
244
+ echo "Leither storage limit saved as ${selected_gb} GB."
245
+ }
246
+
247
+ install_leither_binary() (
248
+ # Follow http://vzhan.cn/#start.html and its install.sh download layout.
249
+ # Download only the binary and checksum; initialization stays in the final
250
+ # private node directory, never in a temporary directory or release bundle.
251
+ set -euo pipefail
252
+ umask 077
253
+ case "$(uname -s)" in Linux) leither_os=linux ;; Darwin) leither_os=darwin ;; *) echo "Unsupported Leither platform." >&2; exit 1 ;; esac
254
+ case "$(uname -m)" in x86_64|amd64) leither_arch=amd64 ;; arm64|aarch64) leither_arch=arm64 ;; *) echo "Unsupported Leither architecture." >&2; exit 1 ;; esac
255
+ if command -v sha256sum >/dev/null 2>&1; then
256
+ checksum_command=(sha256sum)
257
+ elif command -v shasum >/dev/null 2>&1; then
258
+ checksum_command=(shasum -a 256)
259
+ else
260
+ echo "Leither installation requires sha256sum or shasum." >&2
261
+ exit 1
262
+ fi
263
+ download_dir=$(mktemp -d "${TMPDIR:-/tmp}/lifedrive-leither.XXXXXX")
264
+ staged_binary=""
265
+ trap 'if [[ -n "$staged_binary" ]]; then rm -f -- "$staged_binary"; fi; rm -f -- "$download_dir/versions.html" "$download_dir/Leither" "$download_dir/Leither.sha256"; rmdir -- "$download_dir"' EXIT
266
+ base_url="http://vzhan.cn/mm/Fc1BRTFafOGzq5P8KmkVJqwS2v2"
267
+ echo "Finding the latest Leither release from vzhan.cn..."
268
+ curl -fsSL --retry 3 --connect-timeout 20 --max-time 60 "$base_url/" -o "$download_dir/versions.html"
269
+ latest_version=$(node -e '
270
+ const fs = require("node:fs");
271
+ const html = fs.readFileSync(process.argv[1], "utf8");
272
+ const versions = [...html.matchAll(/href=["\x27]V(\d+\.\d+\.\d+)(?:\/|["\x27])/g)].map(match => match[1]);
273
+ versions.sort((a, b) => {
274
+ const left = a.split(".").map(Number), right = b.split(".").map(Number);
275
+ return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
276
+ });
277
+ if (!versions.length) { console.error("No Leither release was found in the official index."); process.exit(1); }
278
+ process.stdout.write(`V${versions[versions.length - 1]}`);
279
+ ' "$download_dir/versions.html")
280
+ download_url="$base_url/$latest_version/Leither.$leither_os.$leither_arch"
281
+ echo "Downloading Leither $latest_version for $leither_os/$leither_arch..."
282
+ curl -fsSL --retry 3 --connect-timeout 20 --max-time 600 "$download_url" -o "$download_dir/Leither"
283
+ curl -fsSL --retry 3 --connect-timeout 20 --max-time 60 "$download_url.sha256" -o "$download_dir/Leither.sha256"
284
+ expected=$(awk 'NR==1 {print $1}' "$download_dir/Leither.sha256")
285
+ actual=$("${checksum_command[@]}" "$download_dir/Leither" | awk '{print $1}')
286
+ if [[ ! "$expected" =~ ^[a-fA-F0-9]{64}$ || "$actual" != "$expected" ]]; then
287
+ echo "Leither SHA-256 verification failed; the binary was not installed." >&2
288
+ exit 1
289
+ fi
290
+ # Install a complete binary atomically on the destination filesystem. The
291
+ # hard link fails if another installer has already created Leither.
292
+ staged_binary=$(mktemp "$LEITHER_WORKDIR/.Leither.XXXXXX")
293
+ cat "$download_dir/Leither" > "$staged_binary"
294
+ chmod 700 "$staged_binary"
295
+ node -e 'require("node:fs").linkSync(process.argv[1], process.argv[2]);' "$staged_binary" "$LEITHER_WORKDIR/Leither"
296
+ )
297
+
298
+ leither_service_prerequisites() {
299
+ if [[ $(id -u) == 0 ]]; then
300
+ echo "Run setup as the account that owns the Leither node; sudo is used only to install its system service." >&2
301
+ exit 1
302
+ fi
303
+ command -v sudo >/dev/null || { echo "System service setup requires sudo." >&2; exit 1; }
304
+ case "$(uname -s)" in
305
+ Linux)
306
+ if ! command -v systemctl >/dev/null || [[ ! -d /run/systemd/system ]]; then
307
+ echo "Automatic Leither startup requires Linux with systemd. On other systems, start Leither with your service manager and use --no-install-leither." >&2
308
+ exit 1
309
+ fi
310
+ systemctl show --property=Version --value >/dev/null || { echo "The systemd service manager is not reachable." >&2; exit 1; }
311
+ ;;
312
+ Darwin)
313
+ command -v launchctl >/dev/null && command -v plutil >/dev/null && command -v lsof >/dev/null || { echo "macOS service setup requires launchctl, plutil, and lsof." >&2; exit 1; }
314
+ ;;
315
+ *) echo "Unsupported service platform." >&2; exit 1 ;;
316
+ esac
317
+ }
318
+
319
+ leither_service_pid() {
320
+ case "$(uname -s)" in
321
+ Linux) systemctl show lifedrive-leither.service --property=MainPID --value 2>/dev/null ;;
322
+ Darwin) launchctl print system/uk.inoku.leither 2>/dev/null | awk '/^[[:space:]]*pid = / {print $3}' ;;
323
+ esac
324
+ }
325
+
326
+ leither_service_owns_process() {
327
+ local service_pid executable process_root
328
+ service_pid=$(leither_service_pid) || return 1
329
+ [[ "$service_pid" =~ ^[0-9]+$ && "$service_pid" != 0 ]] || return 1
330
+ if [[ "$(uname -s)" == Linux ]]; then
331
+ executable=$(readlink "/proc/$service_pid/exe") || return 1
332
+ executable="${executable% (deleted)}"
333
+ process_root=$(readlink "/proc/$service_pid/cwd") || return 1
334
+ else
335
+ executable=$(ps -ww -p "$service_pid" -o comm= | sed 's/^[[:space:]]*//') || return 1
336
+ process_root=$(lsof -a -p "$service_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p') || return 1
337
+ fi
338
+ [[ "$executable" == "$LEITHER_WORKDIR/Leither" && "$process_root" == "$LEITHER_WORKDIR" ]]
339
+ }
340
+
341
+ configure_leither_service() (
342
+ set -euo pipefail
343
+ umask 077
344
+ mode="$1"
345
+ leither_service_prerequisites
346
+ service_os=$(uname -s)
347
+ account=$(id -un)
348
+ service_home=$(node -e 'process.stdout.write(require("node:os").userInfo().homedir);')
349
+ service_work=$(mktemp -d "${TMPDIR:-/tmp}/lifedrive-leither-service.XXXXXX")
350
+ trap 'rm -f -- "$service_work/lifedrive-leither.service" "$service_work/leither.plist"; rmdir -- "$service_work"' EXIT
351
+
352
+ if [[ "$service_os" == Linux ]]; then
353
+ unit="$service_work/lifedrive-leither.service"
354
+ installed_unit=/etc/systemd/system/lifedrive-leither.service
355
+ # Reject systemd specifiers, variable expansion, escapes and line breaks.
356
+ # Ordinary spaces in directory names remain supported through quoting.
357
+ case "$LEITHER_WORKDIR$service_home$account" in *$'\n'*|*$'\r'*|*'"'*|*'%'*|*'$'*|*'\'*) echo "Unsupported character in the Leither systemd service path or account." >&2; exit 1 ;; esac
358
+ cat > "$unit" <<UNIT
359
+ [Unit]
360
+ Description=Leither node installed by LifeDrive
361
+ Wants=network-online.target
362
+ After=network-online.target
363
+ [Service]
364
+ Type=simple
365
+ User=$account
366
+ WorkingDirectory="$LEITHER_WORKDIR"
367
+ Environment="HOME=$service_home"
368
+ ExecStart="$LEITHER_WORKDIR/Leither" run
369
+ UMask=0077
370
+ Restart=on-failure
371
+ RestartSec=5
372
+ TimeoutStopSec=60
373
+ [Install]
374
+ WantedBy=multi-user.target
375
+ UNIT
376
+ existing_unit=$(systemctl show lifedrive-leither.service --property=FragmentPath --value 2>/dev/null || true)
377
+ dropins=$(systemctl show lifedrive-leither.service --property=DropInPaths --value 2>/dev/null || true)
378
+ if [[ -n "$dropins" ]] ||
379
+ { [[ -n "$existing_unit" ]] && ! cmp -s "$unit" "$existing_unit"; } ||
380
+ { [[ -e "$installed_unit" || -L "$installed_unit" ]] && ! cmp -s "$unit" "$installed_unit"; }; then
381
+ echo "An existing lifedrive-leither.service has a different configuration. It was not replaced." >&2
382
+ exit 1
383
+ fi
384
+ if [[ "$mode" == running ]] && ! leither_service_owns_process; then
385
+ echo "Leither is already running outside lifedrive-leither.service. Its process was left unchanged." >&2
386
+ echo "Keep its existing service manager, or stop it during a maintenance window and rerun --leither-service --leither-root for this node." >&2
387
+ exit 1
388
+ fi
389
+ if command -v systemd-analyze >/dev/null; then systemd-analyze verify "$unit"; fi
390
+ if [[ -z "$existing_unit" ]]; then
391
+ sudo install -o root -g root -m 644 "$unit" "$installed_unit"
392
+ fi
393
+ sudo systemctl daemon-reload
394
+ sudo systemctl enable lifedrive-leither.service
395
+ if [[ "$mode" == stopped ]]; then sudo systemctl start lifedrive-leither.service; fi
396
+ echo "Leither boot startup enabled: lifedrive-leither.service (user $account)."
397
+ else
398
+ plist="$service_work/leither.plist"
399
+ installed_plist=/Library/LaunchDaemons/uk.inoku.leither.plist
400
+ node - "$account" "$service_home" "$LEITHER_WORKDIR" <<'NODE' > "$plist"
401
+ const [account, home, root] = process.argv.slice(2);
402
+ process.stdout.write(JSON.stringify({
403
+ Label: "uk.inoku.leither",
404
+ UserName: account,
405
+ ProgramArguments: [`${root}/Leither`, "run"],
406
+ WorkingDirectory: root,
407
+ EnvironmentVariables: { HOME: home, PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" },
408
+ RunAtLoad: true,
409
+ KeepAlive: true,
410
+ ThrottleInterval: 5,
411
+ ExitTimeOut: 60,
412
+ Umask: 63,
413
+ StandardOutPath: `${root}/leither-service.log`,
414
+ StandardErrorPath: `${root}/leither-service.log`
415
+ }));
416
+ NODE
417
+ plutil -convert xml1 "$plist"
418
+ plutil -lint "$plist" >/dev/null
419
+ if [[ -e "$installed_plist" || -L "$installed_plist" ]]; then
420
+ if ! cmp -s "$plist" "$installed_plist"; then
421
+ echo "An existing uk.inoku.leither daemon has a different configuration. It was not replaced." >&2
422
+ exit 1
423
+ fi
424
+ elif launchctl print system/uk.inoku.leither >/dev/null 2>&1; then
425
+ echo "An unmanaged uk.inoku.leither daemon is already loaded. It was left unchanged." >&2
426
+ exit 1
427
+ fi
428
+ if [[ "$mode" == running ]] && ! leither_service_owns_process; then
429
+ echo "Leither is already running outside uk.inoku.leither. Its process was left unchanged." >&2
430
+ echo "Keep its existing service manager, or stop it during a maintenance window and rerun --leither-service --leither-root for this node." >&2
431
+ exit 1
432
+ fi
433
+ if [[ ! -e "$installed_plist" ]]; then sudo install -o root -g wheel -m 644 "$plist" "$installed_plist"; fi
434
+ sudo launchctl enable system/uk.inoku.leither
435
+ if [[ "$mode" == stopped ]]; then
436
+ if launchctl print system/uk.inoku.leither >/dev/null 2>&1; then
437
+ sudo launchctl kickstart system/uk.inoku.leither
438
+ else
439
+ sudo launchctl bootstrap system "$installed_plist"
440
+ fi
441
+ fi
442
+ echo "Leither boot startup enabled: uk.inoku.leither (user $account)."
443
+ fi
444
+ )
445
+
446
+ configure_macos_leither_firewall() {
447
+ [[ "$(uname -s)" == Darwin ]] || return 0
448
+ local firewall=/usr/libexec/ApplicationFirewall/socketfilterfw
449
+ local leither_binary firewall_apps firewall_state block_all_state
450
+ [[ -x "$firewall" ]] || {
451
+ echo "LePan setup could not find the macOS Application Firewall tool at $firewall." >&2
452
+ return 1
453
+ }
454
+ command -v sudo >/dev/null 2>&1 || {
455
+ echo "LePan setup requires sudo to allow Leither through the macOS Application Firewall." >&2
456
+ return 1
457
+ }
458
+ leither_binary=$(node -e 'process.stdout.write(require("node:fs").realpathSync(process.argv[1]));' "$LEITHER_WORKDIR/Leither") || {
459
+ echo "LePan setup could not resolve the installed Leither executable." >&2
460
+ return 1
461
+ }
462
+ [[ "$leither_binary" == /* && -x "$leither_binary" ]] || {
463
+ echo "LePan setup could not access the installed Leither executable at $leither_binary." >&2
464
+ return 1
465
+ }
466
+
467
+ echo "Allowing Leither through the macOS Application Firewall..."
468
+ firewall_apps=$("$firewall" --listapps) || {
469
+ echo "LePan setup could not read the macOS Application Firewall rules." >&2
470
+ return 1
471
+ }
472
+ if [[ "$firewall_apps" != *"$leither_binary"* ]]; then
473
+ sudo "$firewall" --add "$leither_binary" || {
474
+ echo "LePan setup could not add Leither to the macOS Application Firewall." >&2
475
+ return 1
476
+ }
477
+ fi
478
+ sudo "$firewall" --unblockapp "$leither_binary" || {
479
+ echo "LePan setup could not allow incoming connections to Leither in the macOS Application Firewall." >&2
480
+ return 1
481
+ }
482
+
483
+ firewall_apps=$("$firewall" --listapps) || return 1
484
+ [[ "$firewall_apps" == *"$leither_binary"* ]] || {
485
+ echo "LePan setup could not verify Leither in the macOS Application Firewall rules." >&2
486
+ return 1
487
+ }
488
+ firewall_state=$("$firewall" --getappblocked "$leither_binary") || return 1
489
+ [[ "$firewall_state" == *"is permitted"* ]] || {
490
+ echo "The macOS Application Firewall still reports Leither as blocked." >&2
491
+ return 1
492
+ }
493
+ echo "macOS Application Firewall allows incoming connections to Leither."
494
+
495
+ block_all_state=$("$firewall" --getblockall 2>/dev/null || true)
496
+ if [[ "$block_all_state" == *"enabled"* ]]; then
497
+ echo "Warning: macOS is configured to block all incoming connections." >&2
498
+ echo "Disable that setting before using LePan from another device." >&2
499
+ fi
500
+ }
501
+
502
+ bootstrap_leither() {
503
+ if (( ! INSTALL_LEITHER || UPGRADE_ONLY || MANAGEMENT_ONLY )); then
504
+ echo "LePan setup requires a running Leither node for this operation." >&2
505
+ echo "Start the existing node, or run a fresh installation without --upgrade or device-management options." >&2
506
+ exit 1
507
+ fi
508
+ leither_service_prerequisites
509
+ if [[ -z "$LEITHER_WORKDIR" ]]; then
510
+ if [[ -e ./Leither || -e ./SystemVars.json || -e ./systemvars.json || -e ./hostkey.cfg ]]; then
511
+ LEITHER_WORKDIR="$PWD"
512
+ elif command -v Leither >/dev/null 2>&1; then
513
+ LEITHER_WORKDIR=$(node -e 'const fs=require("node:fs"), path=require("node:path"); process.stdout.write(path.dirname(fs.realpathSync(process.argv[1])));' "$(command -v Leither)")
514
+ else
515
+ [[ "${HOME:-}" == /* ]] || { echo "Set --leither-root to an absolute installation directory." >&2; exit 1; }
516
+ LEITHER_WORKDIR="$HOME/.local/share/lifedrive/leither"
517
+ fi
518
+ fi
519
+ local new_node=0 node_version node_port storage_pending
520
+ if [[ ! -e "$LEITHER_WORKDIR/Leither" && ! -L "$LEITHER_WORKDIR/Leither" ]]; then
521
+ if [[ -d "$LEITHER_WORKDIR" && -n "$(ls -A "$LEITHER_WORKDIR")" ]]; then
522
+ echo "Leither is missing, but $LEITHER_WORKDIR contains existing files. Choose an empty --leither-root; nothing was replaced." >&2
523
+ exit 1
524
+ fi
525
+ (umask 077; mkdir -p "$LEITHER_WORKDIR")
526
+ new_node=1
527
+ elif [[ ! -x "$LEITHER_WORKDIR/Leither" ]]; then
528
+ echo "Leither exists at $LEITHER_WORKDIR but is not executable. Correct its permissions and rerun setup." >&2
529
+ exit 1
530
+ fi
531
+ LEITHER_WORKDIR=$(cd "$LEITHER_WORKDIR" && pwd -P)
532
+ storage_pending="$LEITHER_WORKDIR/.lifedrive-storage-pending"
533
+ # A failed quota choice or configuration write must be safely retryable. The
534
+ # marker identifies only nodes this installer itself began creating; an
535
+ # arbitrary stopped existing node is never treated as fresh.
536
+ if [[ -f "$storage_pending" && ! -L "$storage_pending" ]]; then new_node=1; fi
537
+ # Validate the selected node's port and check availability before starting it.
538
+ # The process and HTTP version are checked again after startup.
539
+ node_port=$(node -e '
540
+ const fs = require("node:fs"), path = require("node:path"), net = require("node:net");
541
+ try {
542
+ const root = process.argv[1];
543
+ let port = 4800;
544
+ const vars = ["SystemVars.json", "systemvars.json"].map(name => path.join(root, name)).find(file => fs.existsSync(file));
545
+ if (vars) port = JSON.parse(fs.readFileSync(vars, "utf8")).ServicePort ?? port;
546
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid Leither ServicePort");
547
+ const server = net.createServer();
548
+ server.on("error", () => { console.error(`Leither port ${port} is unavailable. Free it or configure a different ServicePort in the selected node’s SystemVars.json.`); process.exitCode = 1; });
549
+ server.listen(port, "127.0.0.1", () => server.close(() => process.stdout.write(String(port))));
550
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
551
+ ' "$LEITHER_WORKDIR") || exit 1
552
+ if (( new_node )); then
553
+ if [[ ! -x "$LEITHER_WORKDIR/Leither" ]]; then install_leither_binary; fi
554
+ if [[ ! -e "$storage_pending" ]]; then (umask 077; : > "$storage_pending"); fi
555
+ if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json" && ! -s "$LEITHER_WORKDIR/hostkey.cfg" && ! -e "$LEITHER_WORKDIR/ds/config" ]]; then
556
+ echo "Initializing the new private Leither node at $LEITHER_WORKDIR..."
557
+ (umask 077; cd "$LEITHER_WORKDIR" && ./Leither init) || { echo "Leither initialization failed; its files were retained at $LEITHER_WORKDIR." >&2; exit 1; }
558
+ fi
559
+ if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json" || ! -s "$LEITHER_WORKDIR/hostkey.cfg" ]]; then
560
+ echo "Leither initialization did not create its configuration and node key. Inspect $LEITHER_WORKDIR before retrying." >&2
561
+ exit 1
562
+ fi
563
+ configure_new_node_storage || {
564
+ echo "Leither storage setup failed; the initialized node was retained but not started." >&2
565
+ exit 1
566
+ }
567
+ rm -f -- "$storage_pending" || { echo "Could not finish the new-node storage configuration." >&2; exit 1; }
568
+ elif [[ -n "$STORAGE_MAX_GB" ]]; then
569
+ echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
570
+ echo "Change an existing node's limit from LePan Settings instead." >&2
571
+ exit 2
572
+ fi
573
+ node_version=$(leither_version "$LEITHER_WORKDIR") || exit 1
574
+ echo "Installing the Leither system service at $LEITHER_WORKDIR..."
575
+ configure_leither_service stopped
576
+ LEITHER_SERVICE_BOOTSTRAPPED=1
577
+ local ready=0 attempt response
578
+ for attempt in {1..30}; do
579
+ response=$(curl --noproxy '*' -fsS --max-time 2 "http://127.0.0.1:$node_port/getvar?name=ver" 2>/dev/null || true)
580
+ if leither_service_owns_process && node -e 'try { process.exit(JSON.parse(process.argv[1]).replace(/^V/, "") === process.argv[2] ? 0 : 1); } catch { process.exit(1); }' "$response" "$node_version"; then
581
+ ready=1
582
+ break
583
+ fi
584
+ sleep 1
585
+ done
586
+ if (( ! ready )); then
587
+ echo "Leither did not become ready. Its files and service were retained." >&2
588
+ echo "Linux: journalctl -u lifedrive-leither.service. macOS: $LEITHER_WORKDIR/leither-service.log." >&2
589
+ exit 1
590
+ fi
591
+ echo "Leither V$node_version is ready on port $node_port. Continuing LePan installation."
592
+ }
593
+
594
+ running_pids=$(find_leither_pids)
595
+ if [[ -z "$running_pids" ]]; then
596
+ bootstrap_leither
597
+ running_pids=$(find_leither_pids)
598
+ fi
599
+ if [[ -n "$STORAGE_MAX_GB" ]] && (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then
600
+ echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
601
+ echo "Change an existing node's limit from LePan Settings instead." >&2
602
+ exit 2
603
+ fi
604
+
605
+ running_roots=()
606
+ while IFS= read -r leither_pid; do
607
+ [[ "$leither_pid" =~ ^[0-9]+$ ]] || continue
608
+ leither_executable=""
609
+ leither_cwd=""
610
+ if [[ -e "/proc/$leither_pid/exe" ]]; then
611
+ leither_executable=$(readlink "/proc/$leither_pid/exe" 2>/dev/null || true)
612
+ leither_executable="${leither_executable% (deleted)}"
613
+ fi
614
+ if [[ -z "$leither_executable" ]]; then
615
+ leither_executable=$(ps -ww -p "$leither_pid" -o comm= 2>/dev/null | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' || true)
616
+ fi
617
+ if [[ -z "$leither_executable" || "$leither_executable" != /* ]]; then
618
+ leither_command=$(ps -p "$leither_pid" -o args= 2>/dev/null | sed -e 's/^[[:space:]]*//' || true)
619
+ command_executable="${leither_command%% *}"
620
+ if [[ "$command_executable" == /* ]]; then
621
+ leither_executable="$command_executable"
622
+ fi
623
+ fi
624
+ if [[ -L "/proc/$leither_pid/cwd" ]]; then
625
+ leither_cwd=$(readlink "/proc/$leither_pid/cwd" 2>/dev/null || true)
626
+ fi
627
+ if [[ -z "$leither_cwd" ]] && command -v lsof >/dev/null 2>&1; then
628
+ leither_cwd=$(lsof -a -p "$leither_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 || true)
629
+ fi
630
+
631
+ discovered_root=""
632
+ if [[ "$leither_executable" == /* && "${leither_executable##*/}" == "Leither" ]]; then
633
+ executable_dir="${leither_executable%/*}"
634
+ if [[ -x "$executable_dir/Leither" ]]; then
635
+ discovered_root=$(cd "$executable_dir" && pwd -P)
636
+ fi
637
+ elif [[ -n "$leither_cwd" && -x "$leither_cwd/Leither" ]]; then
638
+ discovered_root=$(cd "$leither_cwd" && pwd -P)
639
+ fi
640
+
641
+ if [[ -n "$discovered_root" ]]; then
642
+ duplicate=0
643
+ for known_root in "${running_roots[@]:-}"; do
644
+ if [[ "$known_root" == "$discovered_root" ]]; then duplicate=1; break; fi
645
+ done
646
+ if (( ! duplicate )); then running_roots+=("$discovered_root"); fi
647
+ fi
648
+ done <<< "$running_pids"
649
+
650
+ if (( ${#running_roots[@]} == 0 )); then
651
+ echo "LePan installation stopped: Leither is running, but its root directory could not be determined." >&2
652
+ echo "Ensure the account running setup can inspect the Leither process." >&2
653
+ exit 1
654
+ fi
655
+
656
+ if [[ -n "$LEITHER_WORKDIR" ]]; then
657
+ if [[ ! -d "$LEITHER_WORKDIR" ]]; then
658
+ echo "LePan installation stopped: $LEITHER_WORKDIR is not a directory." >&2
659
+ exit 1
660
+ fi
661
+ requested_root=$(cd "$LEITHER_WORKDIR" && pwd -P)
662
+ root_is_running=0
663
+ for known_root in "${running_roots[@]}"; do
664
+ if [[ "$known_root" == "$requested_root" ]]; then root_is_running=1; break; fi
665
+ done
666
+ if (( ! root_is_running )); then
667
+ echo "LePan installation stopped: no running Leither service uses $requested_root." >&2
668
+ exit 1
669
+ fi
670
+ LEITHER_WORKDIR="$requested_root"
671
+ elif (( ${#running_roots[@]} == 1 )); then
672
+ LEITHER_WORKDIR="${running_roots[0]}"
673
+ else
674
+ echo "LePan installation stopped: more than one Leither service is running." >&2
675
+ printf ' %s\n' "${running_roots[@]}" >&2
676
+ echo "Rerun with --leither-root and one of the directories above." >&2
677
+ exit 1
678
+ fi
679
+
680
+ if [[ ! -x "$LEITHER_WORKDIR/Leither" ]]; then
681
+ echo "LePan installation stopped: the running Leither executable is not accessible at $LEITHER_WORKDIR/Leither." >&2
682
+ exit 1
683
+ fi
684
+ echo "Found running Leither service at $LEITHER_WORKDIR"
685
+ leither_version "$LEITHER_WORKDIR" >/dev/null || exit 1
686
+
687
+ if (( LEITHER_SERVICE_ONLY )); then
688
+ if (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then configure_leither_service running; fi
689
+ configure_macos_leither_firewall
690
+ echo "Leither system service setup is complete. LePan application files were not changed."
691
+ exit 0
692
+ fi
693
+
694
+ if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && "$(sed -n '1p' "$LEITHER_WORKDIR/lifeDrive.owner")" != "lifedrive-key-auth-v1" ]]; then
695
+ echo "LePan installation stopped: the existing prototype uses retired password-owner state." >&2
696
+ echo "This key-auth release intentionally requires a fresh LePan owner state." >&2
697
+ exit 1
698
+ fi
699
+ if [[ -s "$LEITHER_WORKDIR/lifeDrive.owner" && -z "$HOUSEHOLD_CONFIG" ]] && (( ! UPGRADE_ONLY && ! MANAGEMENT_ONLY )); then
700
+ echo "LePan installation stopped: an initialized LePan already exists at $LEITHER_WORKDIR." >&2
701
+ echo "Use --upgrade for application files or --add-device for another browser." >&2
702
+ exit 1
703
+ fi
704
+
705
+ if (( UPGRADE_ONLY )); then
706
+ if [[ ! -d "$LEITHER_WORKDIR/lifeDrive" ]] || { [[ ! -s "$LEITHER_WORKDIR/lifeDrive.owner" ]] && [[ ! -s "$LEITHER_WORKDIR/lifeDrive.households.json" ]]; }; then
707
+ echo "LePan upgrade stopped: no complete existing installation was found at $LEITHER_WORKDIR." >&2
708
+ echo "Install it first with: npx --yes @inoku/lepan@latest" >&2
709
+ exit 1
710
+ fi
711
+ echo "Existing LePan installation found. Its device keys, address, and drive data will be preserved."
712
+ fi
713
+
714
+ INSTALL_TEMP=$(mktemp -d "${TMPDIR:-/tmp}/lifedrive-install.XXXXXX")
715
+ cleanup() {
716
+ case "$INSTALL_TEMP" in
717
+ "${TMPDIR:-/tmp}"/lifedrive-install.*) rm -rf -- "$INSTALL_TEMP" ;;
718
+ esac
719
+ }
720
+ trap cleanup EXIT HUP INT TERM
721
+
722
+ if [[ "$RELEASE_BASE" == file://* ]]; then
723
+ echo "Loading the LePan release included with the npm package..."
724
+ else
725
+ echo "Downloading the latest LePan release from GitHub..."
726
+ fi
727
+ curl -fsSL --retry 3 --connect-timeout 20 -o "$INSTALL_TEMP/$ARCHIVE_NAME" "$RELEASE_BASE/$ARCHIVE_NAME"
728
+ curl -fsSL --retry 3 --connect-timeout 20 -o "$INSTALL_TEMP/$CHECKSUM_NAME" "$RELEASE_BASE/$CHECKSUM_NAME"
729
+
730
+ expected_checksum=$(awk 'NR==1 {print $1}' "$INSTALL_TEMP/$CHECKSUM_NAME")
731
+ if command -v sha256sum >/dev/null 2>&1; then
732
+ actual_checksum=$(sha256sum "$INSTALL_TEMP/$ARCHIVE_NAME" | awk '{print $1}')
733
+ elif command -v shasum >/dev/null 2>&1; then
734
+ actual_checksum=$(shasum -a 256 "$INSTALL_TEMP/$ARCHIVE_NAME" | awk '{print $1}')
735
+ else
736
+ echo "LePan installation requires sha256sum or shasum." >&2
737
+ exit 1
738
+ fi
739
+ if [[ ! "$expected_checksum" =~ ^[a-fA-F0-9]{64}$ || "$actual_checksum" != "$expected_checksum" ]]; then
740
+ echo "LePan release verification failed; the downloaded bundle was not installed." >&2
741
+ exit 1
742
+ fi
743
+
744
+ mkdir -p "$INSTALL_TEMP/bundle"
745
+ tar -xzf "$INSTALL_TEMP/$ARCHIVE_NAME" -C "$INSTALL_TEMP/bundle"
746
+ if [[ ! -x "$INSTALL_TEMP/bundle/lifeDrive/lifeDrive-setup.sh" || ! -f "$INSTALL_TEMP/bundle/lifeDrive/main.go" ]]; then
747
+ echo "LePan release verification failed; required files are missing." >&2
748
+ exit 1
749
+ fi
750
+
751
+ if (( ! MANAGEMENT_ONLY )); then configure_macos_leither_firewall; fi
752
+
753
+ if (( MANAGEMENT_ONLY )); then
754
+ export LIFEDRIVE_WORKDIR="$LEITHER_WORKDIR"
755
+ export LIFEDRIVE_LEITHER_PATH="$LEITHER_WORKDIR/Leither"
756
+ management_command=(/bin/bash "$INSTALL_TEMP/bundle/lifeDrive/lifeDrive-setup.sh")
757
+ if (( setup_arg_count )); then management_command+=("${setup_command_args[@]}"); fi
758
+ "${management_command[@]}"
759
+ exit 0
760
+ fi
761
+
762
+ backup_dir="$LEITHER_WORKDIR/deploy-backups/lifedrive-$(date -u +%Y%m%dT%H%M%SZ)"
763
+ if [[ -d "$LEITHER_WORKDIR/lifeDrive" ]]; then
764
+ mkdir -p "$backup_dir"
765
+ if [[ -d "$LEITHER_WORKDIR/lifeDrive" ]]; then cp -R "$LEITHER_WORKDIR/lifeDrive" "$backup_dir/"; fi
766
+ echo "Previous LePan files backed up to $backup_dir"
767
+ fi
768
+
769
+ mkdir -p "$LEITHER_WORKDIR/lifeDrive"
770
+ # Remove the previous top-level JavaScript MApp entries before installing the
771
+ # Go dispatcher. Web assets below their hashed asset directory are retained.
772
+ old_entry_files=(
773
+ access_check.js access_get_security_mode.js drive_get_state.js drive_list.js drive_open.js
774
+ file_get_info.js file_read.js folder_create.js health.js object_copy.js object_move.js
775
+ object_purge.js object_trash.js server_capabilities.js trash_list.js trash_restore.js
776
+ upload_abort.js upload_begin.js upload_complete.js upload_write.js access_check.test.mjs
777
+ lifeDrive-install-service.sh
778
+ )
779
+ for old_entry in "${old_entry_files[@]}"; do
780
+ old_path="$LEITHER_WORKDIR/lifeDrive/$old_entry"
781
+ if [[ -f "$old_path" ]]; then rm -f -- "$old_path"; fi
782
+ done
783
+ # Remove the obsolete nested web build. The Leither entry bootstrap resolves
784
+ # the current top-level browser objects below /mm/<app-mid>:<version>/.
785
+ old_web_dir="$LEITHER_WORKDIR/lifeDrive/web"
786
+ if [[ -d "$old_web_dir" ]]; then rm -rf -- "$old_web_dir"; fi
787
+ # Hosted-demo assets were accidentally included in the 0.7.8 release. They
788
+ # are not used by the native app and collide with Leither's MiMei short names.
789
+ old_demo_assets="$LEITHER_WORKDIR/lifeDrive/inoku"
790
+ if [[ -d "$old_demo_assets" ]]; then rm -rf -- "$old_demo_assets"; fi
791
+ # Tahoe V0.24.05 derives the same application-entry short name, "index", from
792
+ # index.html and index_entry.js. Remove the rejected TweetWeb-era filename when
793
+ # upgrading to the distinct top-level browser object lifedrive.js.
794
+ old_browser_entry="$LEITHER_WORKDIR/lifeDrive/index_entry.js"
795
+ if [[ -f "$old_browser_entry" ]]; then rm -f -- "$old_browser_entry"; fi
796
+ # CSS is embedded in index.html. An index.css object collides with index.html
797
+ # under the same V0.24.05 short-name normalization.
798
+ old_browser_style="$LEITHER_WORKDIR/lifeDrive/index.css"
799
+ if [[ -f "$old_browser_style" ]]; then rm -f -- "$old_browser_style"; fi
800
+ cp -R "$INSTALL_TEMP/bundle/lifeDrive/." "$LEITHER_WORKDIR/lifeDrive/"
801
+ if [[ -d "$INSTALL_TEMP/bundle/identity" ]]; then
802
+ mkdir -p "$LEITHER_WORKDIR/lifedrive-identity"
803
+ for identity_asset in "$INSTALL_TEMP/bundle/identity/"*; do
804
+ identity_name=$(basename "$identity_asset")
805
+ cp "$identity_asset" "$LEITHER_WORKDIR/lifedrive-identity/.$identity_name.new"
806
+ mv "$LEITHER_WORKDIR/lifedrive-identity/.$identity_name.new" "$LEITHER_WORKDIR/lifedrive-identity/$identity_name"
807
+ done
808
+ chmod 700 "$LEITHER_WORKDIR/lifedrive-identity"
809
+ fi
810
+ chmod 700 "$LEITHER_WORKDIR/lifeDrive"
811
+ chmod 700 "$LEITHER_WORKDIR/lifeDrive/"*.sh
812
+
813
+ export LIFEDRIVE_WORKDIR="$LEITHER_WORKDIR"
814
+ export LIFEDRIVE_LEITHER_PATH="$LEITHER_WORKDIR/Leither"
815
+ if (( HOUSEHOLD_SETUP )); then
816
+ /bin/bash "$LEITHER_WORKDIR/lifedrive-identity/setup-node.sh" "$LEITHER_WORKDIR"
817
+ exit 0
818
+ fi
819
+ if [[ -n "$HOUSEHOLD_CONFIG" ]]; then
820
+ /bin/bash "$LEITHER_WORKDIR/lifedrive-identity/install.sh" "$HOUSEHOLD_CONFIG"
821
+ exit 0
822
+ fi
823
+ if [[ -s "$LEITHER_WORKDIR/lifeDrive.households.json" ]]; then
824
+ /bin/bash "$LEITHER_WORKDIR/lifeDrive/lifeDrive-upgrade-household.sh"
825
+ exit 0
826
+ fi
827
+ setup_command=("$LEITHER_WORKDIR/lifeDrive/lifeDrive-setup.sh")
828
+ if (( setup_arg_count )); then
829
+ setup_command+=("${setup_command_args[@]}")
830
+ fi
831
+ "${setup_command[@]}"