@inoku/lepan 0.7.64 → 0.7.65

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
 
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
@@ -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: 44150306 bytes
4
+ sha256: ef901f174a43f1a7288516db7834821dbdde61a2eb46d4916c82f6ec880972cd
@@ -1 +1 @@
1
- d86c1bc223d01cb0ba01afcdc5a992ac11e5c5f33fb9196c8f8d0faa345638d1 lifedrive-bundle.tar.gz
1
+ ef901f174a43f1a7288516db7834821dbdde61a2eb46d4916c82f6ec880972cd lifedrive-bundle.tar.gz
@@ -164,13 +164,13 @@ 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
175
  echo
176
176
  echo "Choose the maximum hard-drive space Leither may use for LePan and any other data on this node."
@@ -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,7 @@ try {
241
252
  throw error;
242
253
  }
243
254
  NODE
244
- echo "Leither storage limit saved as ${selected_gb} GB."
255
+ echo "Leither storage limit saved as ${STORAGE_MAX_GB} GB."
245
256
  }
246
257
 
247
258
  install_leither_binary() (
@@ -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 "Restarting the newly created Leither node to activate its storage limit..."
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 "Leither restarted with the ${STORAGE_MAX_GB} GB storage 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
@@ -759,14 +841,60 @@ if (( MANAGEMENT_ONLY )); then
759
841
  exit 0
760
842
  fi
761
843
 
844
+ # Existing installations keep their paths (including publisher and service
845
+ # configuration). Fresh installs group application payloads under lepan while
846
+ # retaining the application name used to derive its published identity.
847
+ lepan_dir="$LEITHER_WORKDIR/lepan"
848
+ if [[ ! -d "$LEITHER_WORKDIR/lifeDrive" || -L "$LEITHER_WORKDIR/lifeDrive" ]]; then
849
+ if [[ -L "$lepan_dir" || ( -e "$lepan_dir" && ! -d "$lepan_dir" ) ]]; then
850
+ echo "LePan installation stopped: $lepan_dir must be a regular directory." >&2
851
+ exit 1
852
+ fi
853
+ if [[ -L "$lepan_dir/.installer-layout-v1" || ( -e "$lepan_dir/.installer-layout-v1" && ! -f "$lepan_dir/.installer-layout-v1" ) ]]; then
854
+ echo "LePan installation stopped: invalid layout marker in $lepan_dir." >&2
855
+ exit 1
856
+ fi
857
+ if [[ -d "$lepan_dir" && ! -f "$lepan_dir/.installer-layout-v1" && -n "$(ls -A "$lepan_dir")" ]]; then
858
+ echo "LePan installation stopped: $lepan_dir contains files from an unknown installation." >&2
859
+ exit 1
860
+ fi
861
+ # Stable links keep existing version readers and identity-service paths valid.
862
+ # Refuse conflicting files or links before changing either payload.
863
+ for payload_name in lifeDrive lifedrive-identity; do
864
+ legacy_path="$LEITHER_WORKDIR/$payload_name"
865
+ payload_path="$lepan_dir/$payload_name"
866
+ if [[ -L "$payload_path" || ( -e "$payload_path" && ! -d "$payload_path" ) ]]; then
867
+ echo "LePan installation stopped: unexpected payload at $payload_path." >&2
868
+ exit 1
869
+ fi
870
+ if [[ -L "$legacy_path" ]]; then
871
+ if [[ "$(readlink "$legacy_path")" != "lepan/$payload_name" ]]; then
872
+ echo "LePan installation stopped: $legacy_path points outside the managed LePan layout." >&2
873
+ exit 1
874
+ fi
875
+ elif [[ -e "$legacy_path" ]]; then
876
+ echo "LePan installation stopped: conflicting installation at $legacy_path." >&2
877
+ exit 1
878
+ fi
879
+ done
880
+ (umask 077; mkdir -p "$lepan_dir"; : > "$lepan_dir/.installer-layout-v1")
881
+ for payload_name in lifeDrive lifedrive-identity; do
882
+ (umask 077; mkdir -p "$lepan_dir/$payload_name")
883
+ if [[ ! -L "$LEITHER_WORKDIR/$payload_name" ]]; then
884
+ ln -s "lepan/$payload_name" "$LEITHER_WORKDIR/$payload_name"
885
+ fi
886
+ done
887
+ echo "LePan application files: $lepan_dir"
888
+ fi
889
+
762
890
  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
891
+ if [[ -d "$LEITHER_WORKDIR/lifeDrive" && -n "$(ls -A "$LEITHER_WORKDIR/lifeDrive/")" ]]; then
892
+ mkdir -p "$backup_dir/lifeDrive"
893
+ # Copy the contents rather than backing up only the compatibility link.
894
+ cp -R "$LEITHER_WORKDIR/lifeDrive/." "$backup_dir/lifeDrive/"
766
895
  echo "Previous LePan files backed up to $backup_dir"
767
896
  fi
768
897
 
769
- mkdir -p "$LEITHER_WORKDIR/lifeDrive"
770
898
  # Remove the previous top-level JavaScript MApp entries before installing the
771
899
  # Go dispatcher. Web assets below their hashed asset directory are retained.
772
900
  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.65",
4
4
  "description": "Install LePan and its Leither runtime on Linux, macOS, or Windows",
5
5
  "os": [
6
6
  "linux",