@inoku/lepan 0.7.64 → 0.7.66

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/README.md CHANGED
@@ -30,9 +30,11 @@ To upgrade an existing key-auth installation without changing its authorized dev
30
30
  npx --yes @inoku/lepan@latest --upgrade
31
31
  ```
32
32
 
33
- The installer reuses a running Leither node and finds its directory automatically. If none is running, Linux/macOS fresh setup first looks for Leither in the selected directory, the current directory, or on `PATH`; otherwise it uses `~/.local/share/lifedrive/leither`. Windows uses `%ProgramData%\LifeDrive\Leither` unless `--leither-root` selects another directory. It downloads the matching binary from the [official Leither distribution](http://vzhan.cn/#start.html), verifies its SHA-256 checksum, initializes local configuration and keys, installs boot startup, and waits for the managed process and local version endpoint before continuing. Existing executables, configuration, and keys are never replaced.
33
+ The installer reuses a running Leither node and finds its directory automatically. If none is running, Linux/macOS fresh setup first looks for Leither in the selected directory, the current directory, or on `PATH`; otherwise it uses `~/.local/share/leither`. Windows uses `%ProgramData%\LifeDrive\Leither` unless `--leither-root` selects another directory. It downloads the matching binary from the [official Leither distribution](http://vzhan.cn/#start.html), verifies its SHA-256 checksum, initializes local configuration and keys, installs boot startup, and waits for the managed process and local version endpoint before continuing. Existing executables, configuration, and keys are never replaced.
34
34
 
35
- When the installer creates a new node, it shows the free space on the selected drive and asks how many whole decimal gigabytes Leither may use. The default is **100 GB**. This is a node-wide maximum covering LePan and every other application stored by that Leither node; it does not reserve or preallocate the space. The installer refuses a value larger than the space currently available and saves the choice before Leither first starts. For unattended setup, pass `--storage-max-gb GB` or set `LIFEDRIVE_STORAGE_MAX_GB`. Existing nodes and upgrades keep their current limit; change those from LePan Settings.
35
+ Fresh Linux/macOS installations keep LePan application files in `<Leither root>/lepan` (by default `~/.local/share/leither/lepan`). This also applies to the macOS setup package. The folder contains `lifeDrive/` and `lifedrive-identity/`; compatibility links in the node root preserve the published app identity and service paths. Private node and household state stays in the node root. Existing installations retain their directories during upgrades.
36
+
37
+ When the installer creates a new node, it shows the free space on the selected drive and asks how many whole decimal gigabytes Leither may use. The default is **100 GB**. This is a node-wide maximum covering LePan and every other application stored by that Leither node; it does not reserve or preallocate the space. The installer refuses a value larger than the space currently available and retains the choice before Leither first starts. On Linux/macOS, the first start creates the node key and datastore configuration; setup then applies the limit and restarts only that newly created, managed node before continuing. If setup is interrupted, rerunning fresh setup finishes the pending storage configuration. For unattended setup, pass `--storage-max-gb GB` or set `LIFEDRIVE_STORAGE_MAX_GB`. Existing nodes and upgrades keep their current limit; change those from LePan Settings.
36
38
 
37
39
  Choose another empty directory for a fresh node, select a stopped node, or select among multiple running instances:
38
40
 
@@ -84,7 +86,31 @@ The Linux/macOS legacy browser-owner setup uses the built-in AV1 registration en
84
86
 
85
87
  LifeDrive requires Leither V0.24.11 or newer. Linux/macOS browser-owner setup creates a separate Ed25519 `sodiumv2` identity for the primary browser. Windows prints a private ten-minute invitation for the first paired phone; later users and devices are managed from a paired device.
86
88
 
87
- After successful npm setup, the installer saves a private `lepan-pairing-<id>.png` and prints its full path. It also displays the QR code when the terminal is wide enough. In the iPhone or Android app, open **Settings → Set up users → Scan QR code** and scan the terminal or the saved image. The PNG is saved in the Leither root on Linux/macOS and its private `.lifedrive-household` directory on Windows. Household invitations expire ten minutes after creation; legacy identity codes contain a device private key, so keep them private and delete the image after pairing. Legacy `--add-device` and `--upgrade` also produce a code when an identity is available; household upgrades preserve existing pairings without issuing new invitations.
89
+ After successful npm setup, the installer saves a private `lepan-pairing-<id>.png` and prints its full path. It also displays the QR code when it fits the terminal's width and height. In the iPhone or Android app, open **Settings → Set up users → Scan QR code** and scan the terminal or the saved image. A private `.json` invitation is saved alongside it for apps without a setup scanner: transfer that file to the phone and use **Choose identity file**. These files are saved in the Leither root on Linux/macOS and its private `.lifedrive-household` directory on Windows. Household invitations expire ten minutes after creation; legacy identity codes contain a device private key, so keep them private and delete the transfer copies after pairing. Legacy `--add-device` and `--upgrade` also produce a code when an identity is available; household upgrades preserve existing pairings without issuing new invitations.
90
+
91
+ ### Renew an expired first-phone invitation
92
+
93
+ Run this again **on the computer running Leither**:
94
+
95
+ ```bash
96
+ npx --yes @inoku/lepan@latest --household
97
+ ```
98
+
99
+ If more than one node is running, also pass `--leither-root` with the same node
100
+ directory used during installation. On Windows, use an administrator terminal.
101
+ The macOS setup package also supports renewal when run again.
102
+
103
+ Before the first owner has been created, setup replaces the previous invitation
104
+ and produces a fresh QR image and import file with a new ten-minute expiry. It
105
+ briefly stops only the managed account service while issuing the invitation,
106
+ then starts it and verifies readiness before displaying the new code. If issuance
107
+ fails, it attempts to restore the service. Leither keeps running; existing node
108
+ keys and drive data are preserved.
109
+
110
+ Setup never replaces an existing owner or an interrupted account claim. If users
111
+ already exist, use **My devices** on a paired phone to invite another device. An
112
+ interrupted first claim must be resumed on the original phone with its original
113
+ invitation. Ordinary `--upgrade` does not renew invitations.
88
114
 
89
115
  An existing installation is backed up before replacement. The explicit `--upgrade` path requires the saved application and key-auth owner state before changing files, synchronizes the same published application MID, and does not rerun device or domain setup. This release intentionally does not migrate the earlier password-owner prototype. Application files are active only after Leither advances the application from `cur` to `last`; a publication timeout leaves the prior `last` version serving users.
90
116
 
package/USER_MANUAL.md CHANGED
@@ -24,7 +24,7 @@ A LifeDrive node needs:
24
24
  | A startup manager | systemd on Linux, launchd on macOS, or Task Scheduler on Windows starts the node at boot and recovers it after crashes. |
25
25
  | The LifeDrive app on your phone | iPhone or Android. The first user is created from a phone. |
26
26
 
27
- LifeDrive never stops or restarts Leither. If more than one Leither node runs on
27
+ Normal LifeDrive upgrades never stop or restart Leither. Linux/macOS fresh setup restarts only the node it just created to activate the selected storage limit. If more than one Leither node runs on
28
28
  the machine, the installer lists them and asks you to choose one with
29
29
  `--leither-root <directory>`.
30
30
 
@@ -53,7 +53,7 @@ The same command also remains available from an Administrator Windows Terminal f
53
53
  The installer finds an existing running Leither node. If none is running, it
54
54
  reuses Leither in the selected directory, current directory, or `PATH`, or
55
55
  downloads and verifies the official runtime for your machine. A new node is
56
- created in `~/.local/share/lifedrive/leither` on Linux/macOS or
56
+ created in `~/.local/share/leither` on Linux/macOS or
57
57
  `%ProgramData%\LifeDrive\Leither` on Windows, initialized with its own private
58
58
  keys, and prepared for automatic boot startup. Before starting the service,
59
59
  setup shows the selected drive's available space and asks for the maximum space
@@ -64,6 +64,8 @@ service, waits for Leither's local version
64
64
  endpoint, verifies the LifeDrive archive, installs LifeDrive and runs terminal
65
65
  setup. Follow its prompts.
66
66
 
67
+ Fresh Linux/macOS installations keep LePan application files in `<Leither root>/lepan` (by default `~/.local/share/leither/lepan`). This also applies to the macOS setup package. The folder contains `lifeDrive/` and `lifedrive-identity/`; compatibility links in the node root preserve the published app identity and service paths. Private node and household state stays in the node root. Existing installations retain their directories during upgrades.
68
+
67
69
  To choose another empty directory or start a stopped existing node:
68
70
 
69
71
  ```bash
package/bin/lifedrive.js CHANGED
@@ -22,24 +22,59 @@ async function showPairingCode(resultPath) {
22
22
  }
23
23
  const directory = fs.readFileSync(`${resultPath}.directory`, "utf8");
24
24
  if (!path.isAbsolute(directory)) throw new Error("Invalid pairing image directory");
25
- const QRCode = require("qrcode");
26
25
  const payload = JSON.stringify(pairing);
26
+ // Only encode fields consumed by the existing Android and iOS setup parsers.
27
+ // The node enforces expiry and returns account details after a successful claim.
28
+ // Keep the full invitation in the import file, and leave legacy identities intact.
29
+ // iOS still requires certificate_sha256 even when Leither transport uses a key.
30
+ const qrPayload = pairing.type === "lifedrive-node-setup"
31
+ ? JSON.stringify(Object.fromEntries([
32
+ "type", "version", "app_id", "node_id", "endpoint", "transport",
33
+ "transport_key", "certificate_sha256", "request_id", "secret",
34
+ ].filter(key => Object.hasOwn(pairing, key)).map(key => [key, pairing[key]])))
35
+ : payload;
36
+ const basename = path.join(directory, `lepan-pairing-${randomUUID()}`);
37
+ const imagePath = `${basename}.png`;
38
+ const identityPath = `${basename}.json`;
39
+ // Older mobile releases support importing this file but have no setup scanner.
40
+ // Keep a private copy even when QR generation succeeds.
41
+ fs.writeFileSync(identityPath, payload, { flag: "wx", mode: 0o600 });
27
42
  const options = { errorCorrectionLevel: "M", margin: 4, scale: 8 };
28
- const png = await QRCode.toBuffer(payload, options);
29
- // Unique names and exclusive creation avoid overwriting another device's
30
- // code or following a pre-existing symlink. Keep this outside the app bundle.
31
- const imagePath = path.join(directory, `lepan-pairing-${randomUUID()}.png`);
32
- fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
33
- process.stdout.write(`\nPair your phone: open LePan → Settings → Set up users → Scan QR code.\n`);
34
- process.stdout.write(`Pairing image saved: ${imagePath}\n`);
35
- process.stdout.write(pairing.type === "lifedrive-node-setup"
36
- ? "Keep this code private. The invitation expires ten minutes after creation.\n"
37
- : "This code contains your device private key. Keep it private and delete the image after pairing.\n");
43
+ const privacyNote = pairing.type === "lifedrive-node-setup"
44
+ ? "Keep it private. This invitation expires ten minutes after creation."
45
+ : "This contains your device private key. Keep it private and delete it after pairing.";
46
+ const renewalHint = pairing.type === "lifedrive-node-setup"
47
+ ? "\nInvitation expired? Run this again on the node:\n npx --yes @inoku/lepan@latest --household\n"
48
+ : "";
49
+ let modules;
50
+ try {
51
+ const QRCode = require("qrcode");
52
+ const png = await QRCode.toBuffer(qrPayload, options);
53
+ modules = QRCode.create(qrPayload, options).modules;
54
+ // Unique names and exclusive creation avoid overwriting another device's
55
+ // code or following a pre-existing symlink. Keep this outside the app bundle.
56
+ fs.writeFileSync(imagePath, png, { flag: "wx", mode: 0o600 });
57
+ } catch {
58
+ // The child no longer prints secrets. Preserve an importable fallback
59
+ // before runInstaller removes the temporary handoff directory.
60
+ process.stderr.write("\nNext: pair your phone\n\nThe QR code could not be created. Use the saved pairing file instead.\n");
61
+ process.stderr.write(` ${identityPath}\n\n`);
62
+ process.stderr.write("Transfer this file to your phone, then select it in LePan > Settings > Set up users > Choose identity file.\n");
63
+ process.stderr.write(`${privacyNote}\n`);
64
+ process.stderr.write(renewalHint);
65
+ return;
66
+ }
67
+ process.stdout.write("\nNext: pair your phone\n\n");
68
+ process.stdout.write(" 1. Open LePan on your iPhone or Android phone.\n");
69
+ process.stdout.write(" 2. Go to Settings > Set up users > Scan QR code.\n");
38
70
  // Small terminal codes use half-height blocks. If the terminal would wrap,
39
- // direct the user to the full-resolution PNG instead of a broken code.
40
- const modules = QRCode.create(payload, options).modules;
71
+ // or scroll the instructions out of view, use the full-resolution PNG.
41
72
  const width = modules.size + options.margin * 2;
42
- if (process.stdout.isTTY && (!process.stdout.columns || process.stdout.columns > width)) {
73
+ const height = Math.ceil(width / 2);
74
+ const fitsTerminal = process.stdout.isTTY && process.stdout.columns > width &&
75
+ process.stdout.rows >= height + 26;
76
+ if (fitsTerminal) {
77
+ process.stdout.write(" 3. Scan the code below.\n\n");
43
78
  // Explicit colors and a four-module quiet zone work on light/dark themes.
44
79
  const dark = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && modules.get(y, x);
45
80
  let terminal = "";
@@ -52,8 +87,15 @@ async function showPairingCode(resultPath) {
52
87
  }
53
88
  process.stdout.write(terminal);
54
89
  } else {
55
- process.stdout.write("Open the saved PNG on this computer to scan it.\n");
90
+ process.stdout.write(" 3. Open the saved QR image on this computer and scan it.\n");
56
91
  }
92
+ process.stdout.write(`\nQR image:\n ${imagePath}\n\n${privacyNote}\n`);
93
+ process.stdout.write("\nNo Scan QR code button? Transfer this pairing file to your phone:\n");
94
+ process.stdout.write(` ${identityPath}\n`);
95
+ process.stdout.write("Then open Settings > Set up users > Choose identity file.\n");
96
+ process.stdout.write("Keep the pairing file private; delete the transfer copy after import.\n");
97
+ process.stdout.write("Keep this computer on and connected while using LePan.\n");
98
+ process.stdout.write(renewalHint);
57
99
  }
58
100
 
59
101
  async function runInstaller(command, args) {
@@ -73,7 +115,7 @@ async function runInstaller(command, args) {
73
115
  try { await showPairingCode(resultPath); }
74
116
  catch {
75
117
  // Parser errors may contain identity text; never print them.
76
- process.stderr.write("LePan is installed, but its pairing QR code could not be created. Use the identity file or invitation shown above.\n");
118
+ process.stderr.write("\nPhone pairing could not be prepared. Use your existing identity file, or create a new setup invitation from the node.\n");
77
119
  }
78
120
  }
79
121
  } finally {
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/lifedrive-bundle.tar.gz
3
- size: 44090184 bytes
4
- sha256: d86c1bc223d01cb0ba01afcdc5a992ac11e5c5f33fb9196c8f8d0faa345638d1
3
+ size: 44139710 bytes
4
+ sha256: 134de0bb85330eb3192e837e5054074969a80cbb4eaa1ee0b7870e55b9a3b310
@@ -1 +1 @@
1
- d86c1bc223d01cb0ba01afcdc5a992ac11e5c5f33fb9196c8f8d0faa345638d1 lifedrive-bundle.tar.gz
1
+ 134de0bb85330eb3192e837e5054074969a80cbb4eaa1ee0b7870e55b9a3b310 lifedrive-bundle.tar.gz
@@ -27,7 +27,7 @@ Windows options:
27
27
  --no-install-leither Require an already running Leither node.
28
28
  --leither-service Configure Leither startup only.
29
29
  --upgrade Upgrade an existing Windows LePan installation.
30
- --household Accepted for parity; Windows uses household mode by default.
30
+ --household Set up household users or renew an unclaimed setup invitation.
31
31
 
32
32
  Run Windows Terminal or PowerShell as Administrator. Windows setup supports
33
33
  native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
@@ -382,8 +382,8 @@ if ($Upgrade) {
382
382
  if (Test-Path -LiteralPath (Join-Path $root "lifeDrive.owner") -PathType Leaf) {
383
383
  throw "This node uses the legacy browser-owner setup, which Windows does not migrate automatically. Its files were left unchanged."
384
384
  }
385
- if (Test-Path -LiteralPath $routes -PathType Leaf) {
386
- throw "An initialized LePan household already exists. Rerun with --upgrade."
385
+ if ((Test-Path -LiteralPath $routes -PathType Leaf) -and -not $Household) {
386
+ throw "A LePan household already exists. Use --household for phone setup, or --upgrade for application updates."
387
387
  }
388
388
  }
389
389
 
@@ -28,7 +28,7 @@ Installer options:
28
28
  --storage-max-gb GB Maximum disk space for a newly installed Leither node (default: 100 GB).
29
29
  --release-base URL Alternate GitHub Release asset base URL.
30
30
  --upgrade Upgrade an existing LePan without changing device authorization.
31
- --household Prepare mobile household setup using the existing node address.
31
+ --household Set up household users or renew an unclaimed setup invitation.
32
32
  --household-config FILE Install mobile-led household setup using a private service configuration.
33
33
 
34
34
  Setup options are forwarded to lifeDrive-setup.sh. Common examples:
@@ -164,22 +164,22 @@ available_storage_gb() {
164
164
  LC_ALL=C df -Pk "$LEITHER_WORKDIR" 2>/dev/null | awk 'NR == 2 && $4 ~ /^[0-9]+$/ { print int(($4 * 1024) / 1000000000) }'
165
165
  }
166
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
- }
167
+ choose_new_node_storage() {
168
+ local available_gb selected_gb default_gb=100
169
+ local pending="$LEITHER_WORKDIR/.lifedrive-storage-pending"
170
+ if [[ -z "$STORAGE_MAX_GB" && -s "$pending" ]]; then
171
+ STORAGE_MAX_GB=$(cat "$pending")
172
+ valid_storage_gb "$STORAGE_MAX_GB" || { echo "Invalid saved Leither storage choice." >&2; return 1; }
173
+ fi
174
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."
175
+ printf '\nStorage\n'
176
+ echo "Set the disk space limit for this node (LePan and other Leither data)."
177
177
  if [[ "$available_gb" =~ ^[0-9]+$ ]]; then
178
- echo "Available now on the selected drive: ${available_gb} GB."
178
+ echo " Available: ${available_gb} GB"
179
179
  fi
180
180
  if [[ -n "$STORAGE_MAX_GB" ]]; then
181
181
  selected_gb="$STORAGE_MAX_GB"
182
- echo "Leither storage limit: ${selected_gb} GB."
182
+ echo " Selected limit: ${selected_gb} GB"
183
183
  elif [[ -t 0 ]]; then
184
184
  while true; do
185
185
  read -r -p "Maximum Leither storage in GB [$default_gb]: " selected_gb
@@ -203,7 +203,18 @@ configure_new_node_storage() {
203
203
  echo "Free space or rerun setup with --storage-max-gb set to a smaller value." >&2
204
204
  return 1
205
205
  fi
206
- node - "$config" "${selected_gb}GB" <<'NODE'
206
+ STORAGE_MAX_GB="$selected_gb"
207
+ # Retain the choice if service startup fails or the installer is interrupted.
208
+ (umask 077; printf '%s\n' "$selected_gb" > "$pending")
209
+ }
210
+
211
+ configure_new_node_storage() {
212
+ local config="$LEITHER_WORKDIR/ds/config"
213
+ [[ -f "$config" && ! -L "$config" ]] || {
214
+ echo "Leither startup did not create a regular datastore configuration at $config." >&2
215
+ return 1
216
+ }
217
+ node - "$config" "${STORAGE_MAX_GB}GB" <<'NODE' || return 1
207
218
  const fs = require("node:fs"), path = require("node:path");
208
219
  const file = process.argv[2], desired = process.argv[3];
209
220
  const info = fs.lstatSync(file);
@@ -241,7 +252,6 @@ try {
241
252
  throw error;
242
253
  }
243
254
  NODE
244
- echo "Leither storage limit saved as ${selected_gb} GB."
245
255
  }
246
256
 
247
257
  install_leither_binary() (
@@ -464,7 +474,8 @@ configure_macos_leither_firewall() {
464
474
  return 1
465
475
  }
466
476
 
467
- echo "Allowing Leither through the macOS Application Firewall..."
477
+ printf '\nNetwork access\n'
478
+ echo "Allowing connections through the macOS firewall..."
468
479
  firewall_apps=$("$firewall" --listapps) || {
469
480
  echo "LePan setup could not read the macOS Application Firewall rules." >&2
470
481
  return 1
@@ -490,7 +501,7 @@ configure_macos_leither_firewall() {
490
501
  echo "The macOS Application Firewall still reports Leither as blocked." >&2
491
502
  return 1
492
503
  }
493
- echo "macOS Application Firewall allows incoming connections to Leither."
504
+ echo " Firewall access ready."
494
505
 
495
506
  block_all_state=$("$firewall" --getblockall 2>/dev/null || true)
496
507
  if [[ "$block_all_state" == *"enabled"* ]]; then
@@ -499,6 +510,77 @@ configure_macos_leither_firewall() {
499
510
  fi
500
511
  }
501
512
 
513
+ wait_for_leither() {
514
+ local version="$1" port="$2" previous_pid="${3:-}" attempt response current_pid
515
+ for attempt in {1..30}; do
516
+ current_pid=$(leither_service_pid || true)
517
+ response=$(curl --noproxy '*' -fsS --max-time 2 "http://127.0.0.1:$port/getvar?name=ver" 2>/dev/null || true)
518
+ if [[ -z "$previous_pid" || "$current_pid" != "$previous_pid" ]] &&
519
+ leither_service_owns_process &&
520
+ node -e 'try { process.exit(JSON.parse(process.argv[1]).replace(/^V/, "") === process.argv[2] ? 0 : 1); } catch { process.exit(1); }' "$response" "$version"; then
521
+ return 0
522
+ fi
523
+ sleep 1
524
+ done
525
+ echo "Leither did not become ready. Its files and service were retained." >&2
526
+ echo "Linux: journalctl -u lifedrive-leither.service. macOS: $LEITHER_WORKDIR/leither-service.log." >&2
527
+ return 1
528
+ }
529
+
530
+ verify_new_node_storage() {
531
+ local report
532
+ report=$(cd "$LEITHER_WORKDIR" && ./Leither repo stat --json 2>/dev/null) || return 1
533
+ # Some Leither versions still return a text table with --json. Read only the
534
+ # numeric active limit; never print the complete node response.
535
+ printf '%s' "$report" | node -e '
536
+ const fs = require("node:fs");
537
+ try {
538
+ const source = fs.readFileSync(0, "utf8");
539
+ let value;
540
+ try {
541
+ const reply = JSON.parse(source), data = reply.data ?? reply;
542
+ value = data.StorageMax ?? data.storageMax;
543
+ } catch {
544
+ value = source.match(/^StorageMax:[ \t]*(\d+)[ \t]*$/m)?.[1];
545
+ }
546
+ if (value === undefined || BigInt(value) !== BigInt(process.argv[1]) * 1000000000n) process.exit(1);
547
+ } catch { process.exit(1); }
548
+ ' "$STORAGE_MAX_GB"
549
+ }
550
+
551
+ finish_new_node_storage() {
552
+ local pending="$LEITHER_WORKDIR/.lifedrive-storage-pending" previous_pid version port
553
+ [[ -f "$pending" && ! -L "$pending" ]] || return 1
554
+ # A restart is permitted only for the new node this installer is still
555
+ # provisioning, never a normal upgrade or another manager's process.
556
+ (( ! UPGRADE_ONLY && ! MANAGEMENT_ONLY && INSTALL_LEITHER )) || {
557
+ echo "Finish the pending fresh installation before upgrading or managing devices." >&2; return 1;
558
+ }
559
+ [[ ! -s "$LEITHER_WORKDIR/lifeDrive.owner" && ! -s "$LEITHER_WORKDIR/lifeDrive.households.json" ]] || {
560
+ echo "Storage setup marker conflicts with an initialized LePan installation." >&2; return 1;
561
+ }
562
+ leither_service_owns_process || { echo "Pending storage setup belongs to a node outside the managed service." >&2; return 1; }
563
+ [[ -s "$LEITHER_WORKDIR/hostkey.cfg" ]] || { echo "Leither startup did not create its node key." >&2; return 1; }
564
+ choose_new_node_storage || return 1
565
+ configure_new_node_storage || return 1
566
+ version=$(leither_version "$LEITHER_WORKDIR") || return 1
567
+ port=$(node -e 'const fs=require("node:fs"),p=require("node:path"),r=process.argv[1];const f=["SystemVars.json","systemvars.json"].map(n=>p.join(r,n)).find(f=>fs.existsSync(f));const v=JSON.parse(fs.readFileSync(f)).ServicePort??4800;if(!Number.isInteger(v)||v<1||v>65535)process.exit(1);process.stdout.write(String(v));' "$LEITHER_WORKDIR") || return 1
568
+ previous_pid=$(leither_service_pid) || return 1
569
+ echo "Applying the storage limit (restarting the newly created node)..."
570
+ case "$(uname -s)" in
571
+ Linux) sudo systemctl restart lifedrive-leither.service || return 1 ;;
572
+ Darwin) sudo launchctl kickstart -k system/uk.inoku.leither || return 1 ;;
573
+ *) return 1 ;;
574
+ esac
575
+ wait_for_leither "$version" "$port" "$previous_pid" || return 1
576
+ verify_new_node_storage || {
577
+ echo "The new Leither process has not confirmed the selected storage limit. Rerun fresh setup to finish." >&2
578
+ return 1
579
+ }
580
+ rm -f -- "$pending" || return 1
581
+ echo " Storage ready: ${STORAGE_MAX_GB} GB limit."
582
+ }
583
+
502
584
  bootstrap_leither() {
503
585
  if (( ! INSTALL_LEITHER || UPGRADE_ONLY || MANAGEMENT_ONLY )); then
504
586
  echo "LePan setup requires a running Leither node for this operation." >&2
@@ -513,7 +595,7 @@ bootstrap_leither() {
513
595
  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
596
  else
515
597
  [[ "${HOME:-}" == /* ]] || { echo "Set --leither-root to an absolute installation directory." >&2; exit 1; }
516
- LEITHER_WORKDIR="$HOME/.local/share/lifedrive/leither"
598
+ LEITHER_WORKDIR="$HOME/.local/share/leither"
517
599
  fi
518
600
  fi
519
601
  local new_node=0 node_version node_port storage_pending
@@ -533,7 +615,11 @@ bootstrap_leither() {
533
615
  # A failed quota choice or configuration write must be safely retryable. The
534
616
  # marker identifies only nodes this installer itself began creating; an
535
617
  # arbitrary stopped existing node is never treated as fresh.
536
- if [[ -f "$storage_pending" && ! -L "$storage_pending" ]]; then new_node=1; fi
618
+ if [[ -L "$storage_pending" || ( -e "$storage_pending" && ! -f "$storage_pending" ) ]]; then
619
+ echo "Invalid Leither storage setup marker at $storage_pending." >&2
620
+ exit 1
621
+ fi
622
+ if [[ -f "$storage_pending" ]]; then new_node=1; fi
537
623
  # Validate the selected node's port and check availability before starting it.
538
624
  # The process and HTTP version are checked again after startup.
539
625
  node_port=$(node -e '
@@ -556,15 +642,17 @@ try {
556
642
  echo "Initializing the new private Leither node at $LEITHER_WORKDIR..."
557
643
  (umask 077; cd "$LEITHER_WORKDIR" && ./Leither init) || { echo "Leither initialization failed; its files were retained at $LEITHER_WORKDIR." >&2; exit 1; }
558
644
  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
645
+ if [[ ! -s "$LEITHER_WORKDIR/SystemVars.json" && ! -s "$LEITHER_WORKDIR/systemvars.json" ]]; then
646
+ echo "Leither initialization did not create its settings. Inspect $LEITHER_WORKDIR before retrying." >&2
561
647
  exit 1
562
648
  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; }
649
+ choose_new_node_storage || exit 1
650
+ # init creates only SystemVars. The first run creates the node key and
651
+ # datastore; a stopped retry may already have that datastore available.
652
+ if [[ -e "$LEITHER_WORKDIR/ds/config" ]]; then
653
+ configure_new_node_storage || exit 1
654
+ rm -f -- "$storage_pending"
655
+ fi
568
656
  elif [[ -n "$STORAGE_MAX_GB" ]]; then
569
657
  echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
570
658
  echo "Change an existing node's limit from LePan Settings instead." >&2
@@ -574,20 +662,7 @@ try {
574
662
  echo "Installing the Leither system service at $LEITHER_WORKDIR..."
575
663
  configure_leither_service stopped
576
664
  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
665
+ wait_for_leither "$node_version" "$node_port" || exit 1
591
666
  echo "Leither V$node_version is ready on port $node_port. Continuing LePan installation."
592
667
  }
593
668
 
@@ -596,12 +671,6 @@ if [[ -z "$running_pids" ]]; then
596
671
  bootstrap_leither
597
672
  running_pids=$(find_leither_pids)
598
673
  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
674
  running_roots=()
606
675
  while IFS= read -r leither_pid; do
607
676
  [[ "$leither_pid" =~ ^[0-9]+$ ]] || continue
@@ -684,6 +753,19 @@ fi
684
753
  echo "Found running Leither service at $LEITHER_WORKDIR"
685
754
  leither_version "$LEITHER_WORKDIR" >/dev/null || exit 1
686
755
 
756
+ storage_pending="$LEITHER_WORKDIR/.lifedrive-storage-pending"
757
+ if [[ -L "$storage_pending" ]]; then
758
+ echo "LePan installation stopped: the storage setup marker must not be a symbolic link." >&2
759
+ exit 1
760
+ fi
761
+ if [[ -f "$storage_pending" ]]; then
762
+ finish_new_node_storage || exit 1
763
+ elif [[ -n "$STORAGE_MAX_GB" ]] && (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then
764
+ echo "--storage-max-gb is only valid when this installer creates a new Leither node." >&2
765
+ echo "Change an existing node's limit from LePan Settings instead." >&2
766
+ exit 2
767
+ fi
768
+
687
769
  if (( LEITHER_SERVICE_ONLY )); then
688
770
  if (( ! LEITHER_SERVICE_BOOTSTRAPPED )); then configure_leither_service running; fi
689
771
  configure_macos_leither_firewall
@@ -719,6 +801,7 @@ cleanup() {
719
801
  }
720
802
  trap cleanup EXIT HUP INT TERM
721
803
 
804
+ printf '\nInstall LePan\n'
722
805
  if [[ "$RELEASE_BASE" == file://* ]]; then
723
806
  echo "Loading the LePan release included with the npm package..."
724
807
  else
@@ -759,14 +842,60 @@ if (( MANAGEMENT_ONLY )); then
759
842
  exit 0
760
843
  fi
761
844
 
845
+ # Existing installations keep their paths (including publisher and service
846
+ # configuration). Fresh installs group application payloads under lepan while
847
+ # retaining the application name used to derive its published identity.
848
+ lepan_dir="$LEITHER_WORKDIR/lepan"
849
+ if [[ ! -d "$LEITHER_WORKDIR/lifeDrive" || -L "$LEITHER_WORKDIR/lifeDrive" ]]; then
850
+ if [[ -L "$lepan_dir" || ( -e "$lepan_dir" && ! -d "$lepan_dir" ) ]]; then
851
+ echo "LePan installation stopped: $lepan_dir must be a regular directory." >&2
852
+ exit 1
853
+ fi
854
+ if [[ -L "$lepan_dir/.installer-layout-v1" || ( -e "$lepan_dir/.installer-layout-v1" && ! -f "$lepan_dir/.installer-layout-v1" ) ]]; then
855
+ echo "LePan installation stopped: invalid layout marker in $lepan_dir." >&2
856
+ exit 1
857
+ fi
858
+ if [[ -d "$lepan_dir" && ! -f "$lepan_dir/.installer-layout-v1" && -n "$(ls -A "$lepan_dir")" ]]; then
859
+ echo "LePan installation stopped: $lepan_dir contains files from an unknown installation." >&2
860
+ exit 1
861
+ fi
862
+ # Stable links keep existing version readers and identity-service paths valid.
863
+ # Refuse conflicting files or links before changing either payload.
864
+ for payload_name in lifeDrive lifedrive-identity; do
865
+ legacy_path="$LEITHER_WORKDIR/$payload_name"
866
+ payload_path="$lepan_dir/$payload_name"
867
+ if [[ -L "$payload_path" || ( -e "$payload_path" && ! -d "$payload_path" ) ]]; then
868
+ echo "LePan installation stopped: unexpected payload at $payload_path." >&2
869
+ exit 1
870
+ fi
871
+ if [[ -L "$legacy_path" ]]; then
872
+ if [[ "$(readlink "$legacy_path")" != "lepan/$payload_name" ]]; then
873
+ echo "LePan installation stopped: $legacy_path points outside the managed LePan layout." >&2
874
+ exit 1
875
+ fi
876
+ elif [[ -e "$legacy_path" ]]; then
877
+ echo "LePan installation stopped: conflicting installation at $legacy_path." >&2
878
+ exit 1
879
+ fi
880
+ done
881
+ (umask 077; mkdir -p "$lepan_dir"; : > "$lepan_dir/.installer-layout-v1")
882
+ for payload_name in lifeDrive lifedrive-identity; do
883
+ (umask 077; mkdir -p "$lepan_dir/$payload_name")
884
+ if [[ ! -L "$LEITHER_WORKDIR/$payload_name" ]]; then
885
+ ln -s "lepan/$payload_name" "$LEITHER_WORKDIR/$payload_name"
886
+ fi
887
+ done
888
+ echo "LePan application files: $lepan_dir"
889
+ fi
890
+
762
891
  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
892
+ if [[ -d "$LEITHER_WORKDIR/lifeDrive" && -n "$(ls -A "$LEITHER_WORKDIR/lifeDrive/")" ]]; then
893
+ mkdir -p "$backup_dir/lifeDrive"
894
+ # Copy the contents rather than backing up only the compatibility link.
895
+ cp -R "$LEITHER_WORKDIR/lifeDrive/." "$backup_dir/lifeDrive/"
766
896
  echo "Previous LePan files backed up to $backup_dir"
767
897
  fi
768
898
 
769
- mkdir -p "$LEITHER_WORKDIR/lifeDrive"
770
899
  # Remove the previous top-level JavaScript MApp entries before installing the
771
900
  # Go dispatcher. Web assets below their hashed asset directory are retained.
772
901
  old_entry_files=(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoku/lepan",
3
- "version": "0.7.64",
3
+ "version": "0.7.66",
4
4
  "description": "Install LePan and its Leither runtime on Linux, macOS, or Windows",
5
5
  "os": [
6
6
  "linux",