@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,91 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+ const { pathToFileURL } = require("node:url");
8
+
9
+ const distributionDirectory = path.resolve(__dirname, "..", "dist");
10
+ const bundlePath = path.join(distributionDirectory, "lifedrive-bundle.tar.gz");
11
+ const checksumPath = `${bundlePath}.sha256`;
12
+
13
+ function windowsArguments(values) {
14
+ const valueOptions = new Map([
15
+ ["--leither-root", "-LeitherRoot"],
16
+ ["--storage-max-gb", "-StorageMaxGB"],
17
+ ]);
18
+ const switches = new Map([
19
+ ["--no-install-leither", "-NoInstallLeither"],
20
+ ["--leither-service", "-LeitherService"],
21
+ ["--upgrade", "-Upgrade"],
22
+ ["--household", "-Household"],
23
+ ["--help", "-Help"],
24
+ ["-h", "-Help"],
25
+ ]);
26
+ const translated = [];
27
+ for (let index = 0; index < values.length; index += 1) {
28
+ const argument = values[index];
29
+ const equals = argument.indexOf("=");
30
+ const option = equals >= 0 ? argument.slice(0, equals) : argument;
31
+ if (valueOptions.has(option)) {
32
+ const value = equals >= 0 ? argument.slice(equals + 1) : values[++index];
33
+ if (!value || value.startsWith("-")) throw new Error(`${option} needs a value.`);
34
+ translated.push(valueOptions.get(option), value);
35
+ } else if (switches.has(argument)) {
36
+ translated.push(switches.get(argument));
37
+ } else {
38
+ throw new Error(`Unsupported Windows installer option: ${argument}`);
39
+ }
40
+ }
41
+ return translated;
42
+ }
43
+
44
+ function main() {
45
+ if (process.platform === "win32") {
46
+ const installerPath = path.join(distributionDirectory, "lifedrive-install.ps1");
47
+ if (![installerPath, bundlePath, checksumPath].every(candidate => fs.existsSync(candidate))) {
48
+ throw new Error("The npm package is incomplete. Reinstall @inoku/lepan and try again.");
49
+ }
50
+ const result = spawnSync(
51
+ "powershell.exe",
52
+ ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", installerPath,
53
+ "-PackageDirectory", distributionDirectory, ...windowsArguments(process.argv.slice(2))],
54
+ { stdio: "inherit", env: process.env }
55
+ );
56
+ if (result.error) throw result.error;
57
+ if (typeof result.status === "number") process.exitCode = result.status;
58
+ else process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ if (process.platform !== "linux" && process.platform !== "darwin") {
63
+ throw new Error("LePan setup supports native Linux, macOS, and x64 Windows Leither servers.");
64
+ }
65
+ const installerPath = path.join(distributionDirectory, "lifedrive-install.sh");
66
+ if (!fs.existsSync("/bin/bash")) {
67
+ throw new Error("LePan setup requires /bin/bash.");
68
+ }
69
+ if (![installerPath, bundlePath, checksumPath].every(candidate => fs.existsSync(candidate))) {
70
+ throw new Error("The npm package is incomplete. Reinstall @inoku/lepan and try again.");
71
+ }
72
+
73
+ const localReleaseBase = pathToFileURL(distributionDirectory).href.replace(/\/$/, "");
74
+ const result = spawnSync(
75
+ "/bin/bash",
76
+ [installerPath, "--release-base", localReleaseBase, ...process.argv.slice(2)],
77
+ { stdio: "inherit", env: process.env }
78
+ );
79
+ if (result.error) throw result.error;
80
+ if (typeof result.status === "number") process.exitCode = result.status;
81
+ else if (result.signal === "SIGINT") process.exitCode = 130;
82
+ else if (result.signal === "SIGTERM") process.exitCode = 143;
83
+ else process.exitCode = 1;
84
+ }
85
+
86
+ try {
87
+ main();
88
+ } catch (error) {
89
+ process.stderr.write(`LePan installer stopped: ${error.message}\n`);
90
+ process.exitCode = 1;
91
+ }
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: package/dist/lifedrive-bundle.tar.gz
3
+ size: 40572222 bytes
4
+ sha256: 05320977b19562ac0e8f1419597dd92def37f0f13ec85767e23514cfc8235e53
@@ -0,0 +1 @@
1
+ 05320977b19562ac0e8f1419597dd92def37f0f13ec85767e23514cfc8235e53 lifedrive-bundle.tar.gz
@@ -0,0 +1,478 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [Parameter(Mandatory = $true)]
4
+ [string]$PackageDirectory,
5
+ [string]$LeitherRoot,
6
+ [Int64]$StorageMaxGB = 0,
7
+ [switch]$NoInstallLeither,
8
+ [switch]$LeitherService,
9
+ [switch]$Upgrade,
10
+ [switch]$Household,
11
+ [switch]$Help
12
+ )
13
+
14
+ $ErrorActionPreference = "Stop"
15
+ $leitherTaskName = "LifeDrive Leither"
16
+ $identityTaskName = "LifeDrive Identity"
17
+ $defaultStorageGB = 100
18
+ $releaseBase = "http://vzhan.cn/mm/Fc1BRTFafOGzq5P8KmkVJqwS2v2"
19
+
20
+ function Show-Usage {
21
+ Write-Host @"
22
+ Usage: npx --yes @inoku/lepan [options]
23
+
24
+ Windows options:
25
+ --leither-root DIR Existing node or directory for a new node.
26
+ --storage-max-gb GB New node storage maximum (default: 100 GB).
27
+ --no-install-leither Require an already running Leither node.
28
+ --leither-service Configure Leither startup only.
29
+ --upgrade Upgrade an existing Windows LePan installation.
30
+ --household Accepted for parity; Windows uses household mode by default.
31
+
32
+ Run Windows Terminal or PowerShell as Administrator. Windows setup supports
33
+ native x64 Windows 10/11 and Windows Server; WSL and Git Bash are not required.
34
+ "@
35
+ }
36
+
37
+ if ($Help) { Show-Usage; return }
38
+ if ($Upgrade -and $LeitherService) { throw "Use -Upgrade and -LeitherService separately." }
39
+ if ($StorageMaxGB -lt 0) { throw "Leither storage must be a positive whole number of gigabytes." }
40
+ if ($StorageMaxGB -eq 0 -and $env:LIFEDRIVE_STORAGE_MAX_GB) {
41
+ $parsedStorage = 0L
42
+ if (-not [Int64]::TryParse($env:LIFEDRIVE_STORAGE_MAX_GB, [ref]$parsedStorage) -or $parsedStorage -lt 1) {
43
+ throw "LIFEDRIVE_STORAGE_MAX_GB must be a positive whole number."
44
+ }
45
+ $StorageMaxGB = $parsedStorage
46
+ }
47
+
48
+ $principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())
49
+ if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
50
+ throw "Run Windows Terminal or PowerShell as Administrator, then rerun the npx command."
51
+ }
52
+ if (-not [Environment]::Is64BitOperatingSystem) {
53
+ throw "The official Leither Windows build requires 64-bit Windows."
54
+ }
55
+ $architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
56
+ if ($architecture -ne "AMD64") {
57
+ throw "The official Leither Windows package currently supports x64 (AMD64) only."
58
+ }
59
+
60
+ function Get-LeitherVersion {
61
+ param([string]$Root)
62
+ $executable = Join-Path $Root "Leither.exe"
63
+ Push-Location $Root
64
+ try {
65
+ $reply = & $executable version --json 2>$null
66
+ if ($LASTEXITCODE -ne 0) { throw "Leither version command failed." }
67
+ } finally {
68
+ Pop-Location
69
+ }
70
+ $parsed = ($reply -join [Environment]::NewLine) | ConvertFrom-Json
71
+ $reported = if ($parsed.data.version) { [string]$parsed.data.version } else { [string]$parsed.version }
72
+ $normalized = $reported.TrimStart("V")
73
+ $version = $null
74
+ if (-not [Version]::TryParse($normalized, [ref]$version) -or $version -lt [Version]"0.24.11") {
75
+ throw "LePan requires Leither V0.24.11 or newer. Existing binaries are never replaced."
76
+ }
77
+ return $version
78
+ }
79
+
80
+ function Get-LeitherPort {
81
+ param([string]$Root)
82
+ $path = @("SystemVars.json", "systemvars.json") | ForEach-Object { Join-Path $Root $_ } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
83
+ if (-not $path) { return 4800 }
84
+ $value = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
85
+ $port = if ($null -ne $value.ServicePort) { [int]$value.ServicePort } else { 4800 }
86
+ if ($port -lt 1 -or $port -gt 65535) { throw "Invalid Leither ServicePort in $path." }
87
+ return $port
88
+ }
89
+
90
+ function Get-RunningLeitherRoots {
91
+ $roots = @()
92
+ Get-CimInstance Win32_Process -Filter "Name='Leither.exe'" -ErrorAction SilentlyContinue | ForEach-Object {
93
+ if ($_.ExecutablePath) {
94
+ $candidate = [IO.Path]::GetFullPath((Split-Path -Parent $_.ExecutablePath))
95
+ if ($roots -notcontains $candidate) { $roots += $candidate }
96
+ }
97
+ }
98
+ return $roots
99
+ }
100
+
101
+ function Protect-NewNodeDirectory {
102
+ param([string]$Path)
103
+ $acl = [Security.AccessControl.DirectorySecurity]::new()
104
+ $acl.SetAccessRuleProtection($true, $false)
105
+ $inheritance = [Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit"
106
+ $propagation = [Security.AccessControl.PropagationFlags]::None
107
+ foreach ($sidText in @("S-1-5-18", "S-1-5-32-544", [Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) {
108
+ $sid = [Security.Principal.SecurityIdentifier]::new($sidText)
109
+ $rule = [Security.AccessControl.FileSystemAccessRule]::new(
110
+ $sid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance,
111
+ $propagation, [Security.AccessControl.AccessControlType]::Allow)
112
+ $acl.AddAccessRule($rule)
113
+ }
114
+ Set-Acl -LiteralPath $Path -AclObject $acl
115
+ }
116
+
117
+ function Grant-SystemTaskAccess {
118
+ param([string]$Path)
119
+ $acl = Get-Acl -LiteralPath $Path
120
+ $inheritance = [Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit"
121
+ $sid = [Security.Principal.SecurityIdentifier]::new("S-1-5-18")
122
+ $rule = [Security.AccessControl.FileSystemAccessRule]::new(
123
+ $sid, [Security.AccessControl.FileSystemRights]::FullControl, $inheritance,
124
+ [Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)
125
+ $acl.SetAccessRule($rule)
126
+ Set-Acl -LiteralPath $Path -AclObject $acl
127
+ }
128
+
129
+ function Assert-PortAvailable {
130
+ param([int]$Port)
131
+ $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $Port)
132
+ try {
133
+ $listener.Start()
134
+ } catch {
135
+ throw "Leither port $Port is unavailable. Free it or configure a different ServicePort in the selected node's SystemVars.json."
136
+ } finally {
137
+ $listener.Stop()
138
+ }
139
+ }
140
+
141
+ function Install-LeitherBinary {
142
+ param([string]$Root)
143
+ $work = Join-Path ([IO.Path]::GetTempPath()) ("lifedrive-leither-" + [Guid]::NewGuid().ToString("N"))
144
+ New-Item -ItemType Directory -Path $work | Out-Null
145
+ try {
146
+ Write-Host "Finding the latest official Leither Windows release..."
147
+ $index = (Invoke-WebRequest -UseBasicParsing -Uri ($releaseBase + "/")).Content
148
+ $versions = [regex]::Matches($index, 'href="V(\d+\.\d+\.\d+)/"') | ForEach-Object { [Version]$_.Groups[1].Value } | Sort-Object
149
+ if (-not $versions) { throw "No Leither release was found in the official index." }
150
+ $latest = "V" + $versions[-1].ToString()
151
+ $artifact = "Leither.windows.amd64"
152
+ $binaryURL = "$releaseBase/$latest/$artifact"
153
+ $downloaded = Join-Path $work "Leither.exe"
154
+ $checksum = Join-Path $work "Leither.sha256"
155
+ Write-Host "Downloading Leither $latest for Windows x64..."
156
+ Invoke-WebRequest -UseBasicParsing -Uri $binaryURL -OutFile $downloaded
157
+ Invoke-WebRequest -UseBasicParsing -Uri ($binaryURL + ".sha256") -OutFile $checksum
158
+ $expected = ((Get-Content -LiteralPath $checksum -Raw).Trim() -split "\s+")[0].ToLowerInvariant()
159
+ $actual = (Get-FileHash -LiteralPath $downloaded -Algorithm SHA256).Hash.ToLowerInvariant()
160
+ if ($expected -notmatch '^[a-f0-9]{64}$' -or $actual -ne $expected) {
161
+ throw "Leither SHA-256 verification failed; the binary was not installed."
162
+ }
163
+ $destination = Join-Path $Root "Leither.exe"
164
+ if (Test-Path -LiteralPath $destination) { throw "Leither.exe already exists and was not replaced." }
165
+ Move-Item -LiteralPath $downloaded -Destination $destination
166
+ } finally {
167
+ Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
168
+ }
169
+ }
170
+
171
+ function Set-NewNodeStorage {
172
+ param([string]$Root, [Int64]$RequestedGB)
173
+ $config = Join-Path $Root "ds\config"
174
+ if (-not (Test-Path -LiteralPath $config -PathType Leaf)) {
175
+ throw "Leither initialization did not create $config."
176
+ }
177
+ $driveRoot = [IO.Path]::GetPathRoot($Root)
178
+ $drive = [IO.DriveInfo]::new($driveRoot)
179
+ $availableGB = [Math]::Floor($drive.AvailableFreeSpace / 1000000000)
180
+ Write-Host ""
181
+ Write-Host "Choose the maximum hard-drive space Leither may use for LePan and other data on this node."
182
+ Write-Host "Available now on the selected drive: $availableGB GB."
183
+ $selected = $RequestedGB
184
+ if ($selected -eq 0) {
185
+ if ([Console]::IsInputRedirected) {
186
+ $selected = $defaultStorageGB
187
+ Write-Host "No interactive input was detected; using the $defaultStorageGB GB default."
188
+ } else {
189
+ while ($true) {
190
+ $answer = Read-Host "Maximum Leither storage in GB [$defaultStorageGB]"
191
+ if ([string]::IsNullOrWhiteSpace($answer)) { $selected = $defaultStorageGB }
192
+ else {
193
+ $candidate = 0L
194
+ if ([Int64]::TryParse($answer, [ref]$candidate) -and $candidate -gt 0) { $selected = $candidate }
195
+ else { Write-Warning "Enter a positive whole number of gigabytes."; continue }
196
+ }
197
+ if ($selected -gt $availableGB) { Write-Warning "That exceeds the $availableGB GB currently available."; continue }
198
+ break
199
+ }
200
+ }
201
+ }
202
+ if ($selected -lt 1) { throw "Leither storage must be a positive whole number of gigabytes." }
203
+ if ($selected -gt $availableGB) { throw "The selected limit ($selected GB) exceeds the $availableGB GB currently available." }
204
+
205
+ $source = [IO.File]::ReadAllText($config)
206
+ $matches = [regex]::Matches($source, '"StorageMax"\s*:\s*"([^"]*)"')
207
+ if ($matches.Count -ne 1) { throw "Leither datastore StorageMax must occur exactly once." }
208
+ $desired = [string]$selected + "GB"
209
+ $value = $matches[0].Groups[1]
210
+ $next = $source.Substring(0, $value.Index) + $desired + $source.Substring($value.Index + $value.Length)
211
+ $verified = $next | ConvertFrom-Json
212
+ if ([string]$verified.Datastore.StorageMax -ne $desired) { throw "Updated Leither storage configuration did not validate." }
213
+ $temporary = $config + ".lifedrive-install-" + [Guid]::NewGuid().ToString("N")
214
+ $backup = $config + ".lifedrive-install-backup-" + [Guid]::NewGuid().ToString("N")
215
+ [IO.File]::WriteAllText($temporary, $next, [Text.UTF8Encoding]::new($false))
216
+ try {
217
+ [IO.File]::Replace($temporary, $config, $backup, $true)
218
+ Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
219
+ } catch {
220
+ Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
221
+ throw
222
+ }
223
+ Write-Host "Leither storage limit saved as $selected GB."
224
+ }
225
+
226
+ function Test-ManagedLeitherTask {
227
+ param($Task, [string]$Root)
228
+ if (-not $Task -or $Task.Actions.Count -ne 1) { return $false }
229
+ $action = $Task.Actions[0]
230
+ $expected = Join-Path $Root "Leither.exe"
231
+ return ([IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($action.Execute)) -eq [IO.Path]::GetFullPath($expected)) -and
232
+ ($action.Arguments -eq "run") -and
233
+ ([IO.Path]::GetFullPath($action.WorkingDirectory) -eq [IO.Path]::GetFullPath($Root))
234
+ }
235
+
236
+ function Register-LeitherTask {
237
+ param([string]$Root, [bool]$StartNow)
238
+ $existing = Get-ScheduledTask -TaskName $leitherTaskName -ErrorAction SilentlyContinue
239
+ if ($existing -and -not (Test-ManagedLeitherTask $existing $Root)) {
240
+ throw "A different scheduled task named '$leitherTaskName' already exists. It was not replaced."
241
+ }
242
+ $action = New-ScheduledTaskAction -Execute (Join-Path $Root "Leither.exe") -Argument "run" -WorkingDirectory $Root
243
+ $trigger = New-ScheduledTaskTrigger -AtStartup
244
+ $taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
245
+ $settingsParameters = @{
246
+ AllowStartIfOnBatteries = $true
247
+ DontStopIfGoingOnBatteries = $true
248
+ ExecutionTimeLimit = [TimeSpan]::Zero
249
+ RestartCount = 999
250
+ RestartInterval = (New-TimeSpan -Minutes 1)
251
+ StartWhenAvailable = $true
252
+ MultipleInstances = "IgnoreNew"
253
+ }
254
+ $settings = New-ScheduledTaskSettingsSet @settingsParameters
255
+ $definition = New-ScheduledTask -Action $action -Trigger $trigger -Principal $taskPrincipal -Settings $settings -Description "Leither node installed by LePan"
256
+ Register-ScheduledTask -TaskName $leitherTaskName -InputObject $definition -Force | Out-Null
257
+ if ($StartNow -and (Get-ScheduledTask -TaskName $leitherTaskName).State -ne "Running") {
258
+ Start-ScheduledTask -TaskName $leitherTaskName
259
+ }
260
+ }
261
+
262
+ function Ensure-LeitherFirewall {
263
+ param([string]$Root, [int]$Port)
264
+ $name = "LifeDrive-Leither"
265
+ $program = Join-Path $Root "Leither.exe"
266
+ $rule = Get-NetFirewallRule -Name $name -ErrorAction SilentlyContinue
267
+ if ($rule) {
268
+ $application = $rule | Get-NetFirewallApplicationFilter
269
+ $portFilter = $rule | Get-NetFirewallPortFilter
270
+ if ([IO.Path]::GetFullPath($application.Program) -ne [IO.Path]::GetFullPath($program) -or [string]$portFilter.LocalPort -ne [string]$Port -or $portFilter.Protocol -ne "TCP") {
271
+ throw "An existing firewall rule named $name has a different program or port. It was not replaced."
272
+ }
273
+ Enable-NetFirewallRule -Name $name | Out-Null
274
+ } else {
275
+ New-NetFirewallRule -Name $name -DisplayName "LePan Leither node" -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol TCP -LocalPort $Port -Program $program | Out-Null
276
+ }
277
+ }
278
+
279
+ function Wait-Leither {
280
+ param([string]$Root, [Version]$Version, [int]$Port)
281
+ for ($attempt = 0; $attempt -lt 45; $attempt++) {
282
+ try {
283
+ $reply = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/getvar?name=ver" -TimeoutSec 2
284
+ $reported = ([string]$reply).TrimStart("V")
285
+ $process = Get-CimInstance Win32_Process -Filter "Name='Leither.exe'" -ErrorAction SilentlyContinue | Where-Object {
286
+ $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath) -eq [IO.Path]::GetFullPath((Join-Path $Root "Leither.exe"))
287
+ }
288
+ if ($process -and $reported -eq $Version.ToString()) { return }
289
+ } catch {}
290
+ Start-Sleep -Seconds 1
291
+ }
292
+ throw "Leither did not become ready. Inspect the '$leitherTaskName' task history."
293
+ }
294
+
295
+ $packageRoot = [IO.Path]::GetFullPath($PackageDirectory)
296
+ $archive = Join-Path $packageRoot "lifedrive-bundle.tar.gz"
297
+ $checksumFile = $archive + ".sha256"
298
+ if (-not (Test-Path -LiteralPath $archive -PathType Leaf) -or -not (Test-Path -LiteralPath $checksumFile -PathType Leaf)) {
299
+ throw "The npm package is incomplete. Reinstall @inoku/lepan and try again."
300
+ }
301
+
302
+ $runningRoots = @(Get-RunningLeitherRoots)
303
+ if ($LeitherRoot) {
304
+ $root = [IO.Path]::GetFullPath($LeitherRoot)
305
+ } elseif ($runningRoots.Count -eq 1) {
306
+ $root = $runningRoots[0]
307
+ } elseif ($runningRoots.Count -gt 1) {
308
+ throw "More than one Leither process is running. Rerun with --leither-root."
309
+ } else {
310
+ $root = Join-Path $env:ProgramData "LifeDrive\Leither"
311
+ }
312
+
313
+ $binary = Join-Path $root "Leither.exe"
314
+ $pending = Join-Path $root ".lifedrive-storage-pending"
315
+ $newNode = -not (Test-Path -LiteralPath $binary -PathType Leaf)
316
+ if (Test-Path -LiteralPath $pending -PathType Leaf) { $newNode = $true }
317
+
318
+ if ($newNode) {
319
+ if ($NoInstallLeither -or $Upgrade) { throw "This operation requires an already running Leither node." }
320
+ if (-not (Test-Path -LiteralPath $root)) {
321
+ New-Item -ItemType Directory -Path $root -Force | Out-Null
322
+ Protect-NewNodeDirectory $root
323
+ }
324
+ $contents = @(Get-ChildItem -LiteralPath $root -Force)
325
+ $allowedRetry = @(".lifedrive-storage-pending", "Leither.exe", "SystemVars.json", "systemvars.json", "hostkey.cfg", "ds")
326
+ if (($contents | Where-Object { $allowedRetry -notcontains $_.Name }).Count -gt 0) {
327
+ throw "Leither is missing, but $root contains other files. Choose an empty --leither-root; nothing was replaced."
328
+ }
329
+ if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { Install-LeitherBinary $root }
330
+ if (-not (Test-Path -LiteralPath $pending)) { New-Item -ItemType File -Path $pending | Out-Null }
331
+ if (-not (Test-Path -LiteralPath (Join-Path $root "SystemVars.json")) -and -not (Test-Path -LiteralPath (Join-Path $root "systemvars.json"))) {
332
+ Write-Host "Initializing the new private Leither node at $root..."
333
+ Push-Location $root
334
+ try {
335
+ & $binary init
336
+ if ($LASTEXITCODE -ne 0) { throw "Leither initialization failed; its files were retained at $root." }
337
+ } finally {
338
+ Pop-Location
339
+ }
340
+ }
341
+ $vars = @("SystemVars.json", "systemvars.json") | ForEach-Object { Join-Path $root $_ } | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
342
+ if (-not $vars -or -not (Test-Path -LiteralPath (Join-Path $root "hostkey.cfg") -PathType Leaf) -or
343
+ -not (Test-Path -LiteralPath (Join-Path $root "ds\config") -PathType Leaf)) {
344
+ throw "Leither initialization is incomplete. Its files were retained at $root for inspection."
345
+ }
346
+ Set-NewNodeStorage $root $StorageMaxGB
347
+ Remove-Item -LiteralPath $pending -Force
348
+ } elseif ($StorageMaxGB -gt 0) {
349
+ throw "--storage-max-gb is only valid when this installer creates a new Leither node. Change an existing limit from LePan Settings."
350
+ }
351
+
352
+ $version = Get-LeitherVersion $root
353
+ $port = Get-LeitherPort $root
354
+ $runningAtRoot = $runningRoots -contains $root
355
+ if ($NoInstallLeither -and -not $runningAtRoot) { throw "--no-install-leither requires the selected node to be running." }
356
+ if ($Upgrade -and -not $runningAtRoot) { throw "--upgrade requires the selected Leither node to be running." }
357
+ if (-not $NoInstallLeither -and -not $Upgrade) {
358
+ Grant-SystemTaskAccess $root
359
+ if (-not $runningAtRoot) { Assert-PortAvailable $port }
360
+ Register-LeitherTask $root (-not $runningAtRoot)
361
+ }
362
+ Ensure-LeitherFirewall $root $port
363
+ if (-not $runningAtRoot) { Wait-Leither $root $version $port }
364
+ Write-Host "Leither V$version is ready at $root on port $port."
365
+
366
+ if ($LeitherService) {
367
+ Write-Host "Leither startup and firewall configuration are complete. LePan files were not changed."
368
+ return
369
+ }
370
+
371
+ # The identity task also runs as SYSTEM, including when Leither itself is
372
+ # managed separately through --no-install-leither.
373
+ Grant-SystemTaskAccess $root
374
+
375
+ $config = Join-Path $root ".lifedrive-household\identity.json"
376
+ $routes = Join-Path $root "lifeDrive.households.json"
377
+ if ($Upgrade) {
378
+ if (-not (Test-Path -LiteralPath $config -PathType Leaf)) {
379
+ throw "LePan upgrade stopped: no Windows household installation was found at $root."
380
+ }
381
+ } else {
382
+ if (Test-Path -LiteralPath (Join-Path $root "lifeDrive.owner") -PathType Leaf) {
383
+ throw "This node uses the legacy browser-owner setup, which Windows does not migrate automatically. Its files were left unchanged."
384
+ }
385
+ if (Test-Path -LiteralPath $routes -PathType Leaf) {
386
+ throw "An initialized LePan household already exists. Rerun with --upgrade."
387
+ }
388
+ }
389
+
390
+ $expectedChecksum = ((Get-Content -LiteralPath $checksumFile -Raw).Trim() -split "\s+")[0].ToLowerInvariant()
391
+ $actualChecksum = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
392
+ if ($expectedChecksum -notmatch '^[a-f0-9]{64}$' -or $actualChecksum -ne $expectedChecksum) {
393
+ throw "LePan release verification failed; the package was not installed."
394
+ }
395
+
396
+ $installTemp = Join-Path ([IO.Path]::GetTempPath()) ("lifedrive-install-" + [Guid]::NewGuid().ToString("N"))
397
+ $extract = Join-Path $installTemp "bundle"
398
+ New-Item -ItemType Directory -Path $extract -Force | Out-Null
399
+ $identityTaskStopped = $false
400
+ try {
401
+ & tar.exe -xzf $archive -C $extract
402
+ if ($LASTEXITCODE -ne 0) { throw "LePan release archive could not be extracted." }
403
+ $sourceApp = Join-Path $extract "lifeDrive"
404
+ $sourceIdentity = Join-Path $extract "identity"
405
+ if (-not (Test-Path -LiteralPath (Join-Path $sourceApp "main.go") -PathType Leaf) -or
406
+ -not (Test-Path -LiteralPath (Join-Path $sourceIdentity "lifedrive-identity-windows-amd64.exe") -PathType Leaf) -or
407
+ -not (Test-Path -LiteralPath (Join-Path $sourceIdentity "setup-node.ps1") -PathType Leaf)) {
408
+ throw "LePan release verification failed; required Windows files are missing."
409
+ }
410
+
411
+ $identityTask = Get-ScheduledTask -TaskName $identityTaskName -ErrorAction SilentlyContinue
412
+ $installedIdentity = Join-Path $root "lifedrive-identity\lifedrive-identity-windows-amd64.exe"
413
+ if ($identityTask) {
414
+ if ($identityTask.Actions.Count -ne 1 -or [IO.Path]::GetFullPath([Environment]::ExpandEnvironmentVariables($identityTask.Actions[0].Execute)) -ne [IO.Path]::GetFullPath($installedIdentity)) {
415
+ throw "A different scheduled task named '$identityTaskName' already exists. It was not stopped."
416
+ }
417
+ if ($identityTask.State -eq "Running") {
418
+ Stop-ScheduledTask -TaskName $identityTaskName
419
+ $identityTaskStopped = $true
420
+ for ($attempt = 0; $attempt -lt 30; $attempt++) {
421
+ if ((Get-ScheduledTask -TaskName $identityTaskName).State -ne "Running") { break }
422
+ Start-Sleep -Seconds 1
423
+ }
424
+ if ((Get-ScheduledTask -TaskName $identityTaskName).State -eq "Running") {
425
+ throw "The existing '$identityTaskName' task did not stop; installed files were not changed."
426
+ }
427
+ }
428
+ }
429
+
430
+ $destinationApp = Join-Path $root "lifeDrive"
431
+ if (Test-Path -LiteralPath $destinationApp -PathType Container) {
432
+ $backupRoot = Join-Path $root ("deploy-backups\lifedrive-" + (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmssZ"))
433
+ New-Item -ItemType Directory -Path $backupRoot -Force | Out-Null
434
+ Copy-Item -LiteralPath $destinationApp -Destination $backupRoot -Recurse
435
+ Write-Host "Previous LePan files backed up to $backupRoot"
436
+ }
437
+ New-Item -ItemType Directory -Path $destinationApp -Force | Out-Null
438
+ foreach ($obsolete in @("web", "inoku", "index_entry.js", "index.css", "lifeDrive-install-service.sh")) {
439
+ Remove-Item -LiteralPath (Join-Path $destinationApp $obsolete) -Recurse -Force -ErrorAction SilentlyContinue
440
+ }
441
+ Get-ChildItem -LiteralPath $sourceApp -Force | ForEach-Object {
442
+ Copy-Item -LiteralPath $_.FullName -Destination $destinationApp -Recurse -Force
443
+ }
444
+
445
+ $destinationIdentity = Join-Path $root "lifedrive-identity"
446
+ New-Item -ItemType Directory -Path $destinationIdentity -Force | Out-Null
447
+ Get-ChildItem -LiteralPath $sourceIdentity -Force | ForEach-Object {
448
+ Copy-Item -LiteralPath $_.FullName -Destination $destinationIdentity -Recurse -Force
449
+ }
450
+ } catch {
451
+ if ($identityTaskStopped) {
452
+ try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
453
+ }
454
+ throw
455
+ } finally {
456
+ Remove-Item -LiteralPath $installTemp -Recurse -Force -ErrorAction SilentlyContinue
457
+ }
458
+
459
+ if ($Upgrade) {
460
+ try {
461
+ & (Join-Path $root "lifedrive-identity\setup-node.ps1") -LeitherRoot $root -Upgrade
462
+ } catch {
463
+ if ($identityTaskStopped) {
464
+ try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
465
+ }
466
+ throw
467
+ }
468
+ Write-Host "LePan Windows upgrade complete. Leither was not restarted."
469
+ return
470
+ }
471
+ try {
472
+ & (Join-Path $root "lifedrive-identity\setup-node.ps1") -LeitherRoot $root
473
+ } catch {
474
+ if ($identityTaskStopped) {
475
+ try { Start-ScheduledTask -TaskName $identityTaskName } catch {}
476
+ }
477
+ throw
478
+ }