@hsusulist/luc 0.2.5 → 0.3.1

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/install.ps1 CHANGED
@@ -43,6 +43,10 @@ $script:UseAnsi = $false
43
43
  $script:InAltScreen = $false
44
44
  $script:Interactive = $false
45
45
  $script:Version = ''
46
+ $script:MaintenanceAction = 'install'
47
+ $script:ExistingComponents = @()
48
+ $script:ExistingInstall = $null
49
+ $script:ScriptDirectory = if ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { $null }
46
50
 
47
51
  # ---------------------------------------------------------------------------
48
52
  # Console helpers
@@ -253,6 +257,153 @@ function Show-SourcePage {
253
257
  }
254
258
  }
255
259
 
260
+ function Get-InstalledDetails {
261
+ param([string]$RequestedPath)
262
+
263
+ $state = $null
264
+ $stateFile = Join-Path (Join-Path $env:LOCALAPPDATA 'LUC') 'state.json'
265
+ if (Test-Path -LiteralPath $stateFile -PathType Leaf) {
266
+ try {
267
+ $state = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json -ErrorAction Stop
268
+ } catch {
269
+ Write-Note "Could not read the install state: $($_.Exception.Message)" 'warn'
270
+ }
271
+ }
272
+
273
+ $path = $RequestedPath
274
+ if (-not $path -and $script:ScriptDirectory -and
275
+ (Test-Path -LiteralPath (Join-Path $script:ScriptDirectory 'luc.exe') -PathType Leaf)) {
276
+ $path = $script:ScriptDirectory
277
+ }
278
+ if (-not $path -and $state -and $state.installPath) { $path = [string]$state.installPath }
279
+ if (-not $path) { $path = $defaultInstallPath }
280
+ $path = Resolve-InstallPathValue $path
281
+ if (-not (Test-Path -LiteralPath (Join-Path $path 'luc.exe') -PathType Leaf)) { return $null }
282
+
283
+ if ($state -and $state.installPath) {
284
+ try {
285
+ $statePath = Resolve-InstallPathValue ([string]$state.installPath)
286
+ if (-not [string]::Equals($statePath, $path, [StringComparison]::OrdinalIgnoreCase)) {
287
+ $state = $null
288
+ }
289
+ } catch {
290
+ Write-Note "Could not resolve the install state path: $($_.Exception.Message)" 'warn'
291
+ $state = $null
292
+ }
293
+ } elseif ($RequestedPath) {
294
+ $state = $null
295
+ }
296
+
297
+ $uninstallKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Luc_is1'
298
+ $registeredPath = $null
299
+ $registeredVersion = $null
300
+ try {
301
+ $registration = Get-ItemProperty -LiteralPath $uninstallKey -ErrorAction Stop
302
+ $registeredPath = [string]$registration.InstallLocation
303
+ $registeredVersion = [string]$registration.DisplayVersion
304
+ } catch { }
305
+ $registeredMatches = $false
306
+ if ($registeredPath) {
307
+ try {
308
+ $registeredMatches = [string]::Equals(
309
+ (Resolve-InstallPathValue $registeredPath), $path, [StringComparison]::OrdinalIgnoreCase)
310
+ if (-not $registeredMatches) { $registeredVersion = $null }
311
+ } catch {
312
+ Write-Note "Could not resolve the Windows installer path: $($_.Exception.Message)" 'warn'
313
+ $registeredPath = $null
314
+ $registeredVersion = $null
315
+ }
316
+ }
317
+ $innoStateMatches = $state -and $state.source -eq 'inno' -and $state.installPath
318
+
319
+ $rawComponents = @()
320
+ if ($state -and $state.components) {
321
+ $rawComponents = @($state.components)
322
+ } else {
323
+ $manifestFile = Join-Path $path '.luc-manifest.json'
324
+ if (Test-Path -LiteralPath $manifestFile -PathType Leaf) {
325
+ try {
326
+ $manifest = Get-Content -LiteralPath $manifestFile -Raw | ConvertFrom-Json -ErrorAction Stop
327
+ if ($manifest.components) { $rawComponents = @($manifest.components) }
328
+ } catch {
329
+ Write-Note "Could not read the install manifest: $($_.Exception.Message)" 'warn'
330
+ }
331
+ }
332
+ }
333
+ if ($rawComponents.Count -eq 0) {
334
+ if ($registeredMatches -or ($innoStateMatches -and -not $registeredPath)) {
335
+ try {
336
+ $saved = (Get-ItemProperty -LiteralPath 'HKCU:\Software\LUC' -Name Components -ErrorAction Stop).Components
337
+ if ($saved) { $rawComponents = @([string]$saved -split '[,\s]+') }
338
+ } catch { }
339
+ }
340
+ }
341
+
342
+ $components = @()
343
+ foreach ($name in $rawComponents) {
344
+ $name = ([string]$name).ToLowerInvariant()
345
+ if ($name -eq 'ailib') { $name = 'ai' }
346
+ if ($optionalComponents -contains $name -and $components -notcontains $name) {
347
+ $components += $name
348
+ }
349
+ }
350
+
351
+ $uninstaller = $null
352
+ if ($innoStateMatches -or $registeredMatches) {
353
+ $uninstaller = Get-ChildItem -LiteralPath $path -Filter 'unins*.exe' -File -ErrorAction SilentlyContinue |
354
+ Select-Object -First 1
355
+ }
356
+ return [pscustomobject]@{
357
+ Path = $path
358
+ Version = if ($state -and $state.version) { [string]$state.version } elseif ($registeredVersion) { $registeredVersion } else { 'unknown' }
359
+ Components = $components
360
+ Uninstaller = $uninstaller
361
+ }
362
+ }
363
+
364
+ function Show-MaintenancePage {
365
+ param($Install)
366
+
367
+ $items = @(
368
+ @{ Action = 'libraries'; Label = 'Install libraries'; Desc = 'Add or remove optional components' },
369
+ @{ Action = 'repair'; Label = 'Fix / reinstall'; Desc = 'Reinstall the current components' }
370
+ )
371
+ if ($Install.Uninstaller) {
372
+ $items += @{ Action = 'uninstall'; Label = 'Uninstall'; Desc = 'Remove LUC from this computer' }
373
+ }
374
+ $items += @{ Action = 'cancel'; Label = 'Cancel'; Desc = 'Exit without making changes' }
375
+ $index = 0
376
+ while ($true) {
377
+ Clear-Tui
378
+ Write-TuiHeader 'LUC is installed' 'Choose an action'
379
+ Write-Tui (" Version {0}" -f $Install.Version)
380
+ Write-Tui (" Folder {0}" -f $Install.Path) '90'
381
+ Write-Tui ''
382
+ for ($i = 0; $i -lt $items.Count; $i++) {
383
+ $marker = if ($i -eq $index) { '>' } else { ' ' }
384
+ $code = if ($i -eq $index) { '1;36' } else { '' }
385
+ Write-Tui (" $marker " + $items[$i].Label) $code
386
+ Write-Tui (" " + $items[$i].Desc) '90'
387
+ Write-Tui ''
388
+ }
389
+ Write-TuiFooter 'Up/Down move Enter select q quit'
390
+ $key = [Console]::ReadKey($true)
391
+ switch ($key.Key) {
392
+ 'UpArrow' { $index = ($index - 1 + $items.Count) % $items.Count }
393
+ 'DownArrow' { $index = ($index + 1) % $items.Count }
394
+ 'Enter' { return $items[$index].Action }
395
+ 'Escape' { return 'cancel' }
396
+ 'Q' { return 'cancel' }
397
+ }
398
+ }
399
+ }
400
+
401
+ function Start-InnoUninstaller {
402
+ param($Install)
403
+ if (-not $Install.Uninstaller) { throw 'The Windows installer uninstaller was not found.' }
404
+ Start-Process -FilePath $Install.Uninstaller.FullName -ArgumentList '/NORESTART' | Out-Null
405
+ }
406
+
256
407
  function Show-LicenseEntry {
257
408
  param([string]$LicenseText)
258
409
  Show-LicensePage $LicenseText
@@ -846,7 +997,7 @@ public static extern bool MoveFileEx(string existingFileName, string newFileName
846
997
  }
847
998
 
848
999
  function Install-ArchivePlan {
849
- param([object[]]$Items)
1000
+ param([object[]]$Items, [string[]]$RemovePaths = @())
850
1001
  $journal = New-Object System.Collections.ArrayList
851
1002
  try {
852
1003
  foreach ($item in $Items) {
@@ -865,6 +1016,13 @@ function Install-ArchivePlan {
865
1016
  try { Move-Item -LiteralPath $item.Staging -Destination $destination -Force }
866
1017
  catch { throw "Cannot write '$destination': $($_.Exception.Message)" }
867
1018
  }
1019
+ foreach ($destination in $RemovePaths) {
1020
+ if (-not (Test-Path -LiteralPath $destination -PathType Leaf)) { continue }
1021
+ $old = "$destination.luc-old-$([guid]::NewGuid().ToString('N').Substring(0, 8))"
1022
+ try { Move-Item -LiteralPath $destination -Destination $old -Force }
1023
+ catch { throw "Cannot remove '$destination'. Close the program using it and try again." }
1024
+ [void]$journal.Add([pscustomobject]@{ Destination = $destination; Old = $old })
1025
+ }
868
1026
  } catch {
869
1027
  $failure = $_
870
1028
  for ($i = $journal.Count - 1; $i -ge 0; $i--) {
@@ -966,6 +1124,26 @@ $downloadTemp = $null
966
1124
  $tempChecksumFile = $null
967
1125
  $zip = $null
968
1126
  try {
1127
+ if ($script:Interactive -and -not $LCodeOnly -and -not $Components) {
1128
+ $script:ExistingInstall = Get-InstalledDetails -RequestedPath $InstallPath
1129
+ if ($script:ExistingInstall) {
1130
+ Enter-AltScreen
1131
+ $script:MaintenanceAction = Show-MaintenancePage $script:ExistingInstall
1132
+ Exit-AltScreen
1133
+ if ($script:MaintenanceAction -eq 'cancel') {
1134
+ Write-Note 'Installation cancelled.' 'warn'
1135
+ exit 2
1136
+ }
1137
+ if ($script:MaintenanceAction -eq 'uninstall') {
1138
+ Start-InnoUninstaller $script:ExistingInstall
1139
+ Write-Note 'LUC uninstaller started.' 'ok'
1140
+ exit 0
1141
+ }
1142
+ $InstallPath = $script:ExistingInstall.Path
1143
+ $script:ExistingComponents = @($script:ExistingInstall.Components)
1144
+ }
1145
+ }
1146
+
969
1147
  $sourceSpecified = $PSBoundParameters.ContainsKey('Source')
970
1148
  if (-not $Source) {
971
1149
  if ($ArchivePath) { $Source = 'local' } else { $Source = 'github' }
@@ -1081,7 +1259,13 @@ try {
1081
1259
  $licenseText = Read-ZipEntryText $zip 'LICENCE'
1082
1260
  if (-not $licenseText) { $licenseText = 'LUC is free software released under the Apache License 2.0.' }
1083
1261
 
1084
- $selected = if ($LCodeOnly) { @('lcode') } else { @() }
1262
+ if ($LCodeOnly) {
1263
+ $selected = @('lcode')
1264
+ } elseif ($script:MaintenanceAction -ne 'install') {
1265
+ $selected = @($script:ExistingComponents)
1266
+ } else {
1267
+ $selected = @()
1268
+ }
1085
1269
  if (-not $LCodeOnly -and $Components) {
1086
1270
  foreach ($item in $Components) {
1087
1271
  foreach ($name in ($item -split '[,\s]+')) {
@@ -1094,7 +1278,8 @@ try {
1094
1278
  }
1095
1279
  }
1096
1280
 
1097
- if (-not $LCodeOnly -and $script:Interactive -and -not $Components) {
1281
+ if (-not $LCodeOnly -and $script:Interactive -and -not $Components -and
1282
+ $script:MaintenanceAction -ne 'repair') {
1098
1283
  Enter-AltScreen
1099
1284
  $picked = Show-ComponentPage -Preselected $selected -LicenseText $licenseText
1100
1285
  Exit-AltScreen
@@ -1115,6 +1300,14 @@ try {
1115
1300
  $InstallPath = Resolve-InstallPathValue $InstallPath
1116
1301
  }
1117
1302
 
1303
+ if (-not $script:ExistingInstall -or
1304
+ -not [string]::Equals($script:ExistingInstall.Path, $InstallPath, [StringComparison]::OrdinalIgnoreCase)) {
1305
+ $script:ExistingInstall = Get-InstalledDetails -RequestedPath $InstallPath
1306
+ }
1307
+ if ($script:ExistingInstall) {
1308
+ $script:ExistingComponents = @($script:ExistingInstall.Components)
1309
+ }
1310
+
1118
1311
  $vsCodeTarget = Join-Path $env:USERPROFILE '.vscode\extensions\hsusulist.luc'
1119
1312
  $binaryName = if ($LCodeOnly) { 'lcode.exe' } else { 'luc.exe' }
1120
1313
  $upgrade = Test-Path -LiteralPath (Join-Path $InstallPath $binaryName) -PathType Leaf
@@ -1140,6 +1333,10 @@ try {
1140
1333
  Exit-AltScreen
1141
1334
  if ($null -eq $picked -or -not $InstallPath) { Write-Note 'Installation cancelled.' 'warn'; exit 2 }
1142
1335
  $selected = @($picked)
1336
+ $InstallPath = Resolve-InstallPathValue $InstallPath
1337
+ $script:ExistingInstall = Get-InstalledDetails -RequestedPath $InstallPath
1338
+ $script:ExistingComponents = if ($script:ExistingInstall) { @($script:ExistingInstall.Components) } else { @() }
1339
+ $upgrade = Test-Path -LiteralPath (Join-Path $InstallPath $binaryName) -PathType Leaf
1143
1340
  }
1144
1341
  }
1145
1342
 
@@ -1149,10 +1346,24 @@ try {
1149
1346
  throw "Installation archive did not produce $binaryName."
1150
1347
  }
1151
1348
 
1349
+ $removePaths = @()
1350
+ if ($upgrade -and -not $LCodeOnly -and $script:ExistingComponents.Count -gt 0) {
1351
+ $previousPlan = Get-ArchivePlan -Zip $zip -SelectedComponents $script:ExistingComponents -Target $InstallPath -VsCodeTarget $vsCodeTarget
1352
+ $newDestinations = @{}
1353
+ foreach ($item in $archivePlan.Items) { $newDestinations[$item.Destination.ToLowerInvariant()] = $true }
1354
+ foreach ($item in $previousPlan.Items) {
1355
+ if (-not $newDestinations.ContainsKey($item.Destination.ToLowerInvariant()) -and
1356
+ (Test-Path -LiteralPath $item.Destination -PathType Leaf)) {
1357
+ $removePaths += $item.Destination
1358
+ }
1359
+ }
1360
+ }
1361
+
1152
1362
  $roots = @($InstallPath)
1153
- if ($selected -contains 'vscode') { $roots += $vsCodeTarget }
1363
+ if (($selected -contains 'vscode') -or ($script:ExistingComponents -contains 'vscode')) { $roots += $vsCodeTarget }
1364
+ $lockItems = @($archivePlan.Items) + @($removePaths | ForEach-Object { @{ Destination = $_ } })
1154
1365
  Write-Step 'Checking for running programs and locked files'
1155
- Test-InstallLocks -Items $archivePlan.Items -Roots $roots
1366
+ Test-InstallLocks -Items $lockItems -Roots $roots
1156
1367
  Write-StepDone 'No running LUC process blocks the install'
1157
1368
 
1158
1369
  Test-DiskSpace $InstallPath $archivePlan.TotalBytes
@@ -1165,17 +1376,19 @@ try {
1165
1376
  Write-StepDone "Extracted $($archivePlan.Items.Count) files"
1166
1377
 
1167
1378
  Write-Step 'Installing files'
1168
- Install-ArchivePlan $archivePlan.Items
1169
- Write-StepDone 'Files installed'
1379
+ Install-ArchivePlan -Items $archivePlan.Items -RemovePaths $removePaths
1380
+ if ($removePaths.Count) { Write-StepDone "Files installed; removed $($removePaths.Count) deselected files" }
1381
+ else { Write-StepDone 'Files installed' }
1170
1382
 
1171
1383
  if (-not (Test-Path -LiteralPath (Join-Path $InstallPath $binaryName) -PathType Leaf)) {
1172
1384
  throw "Installation finished without $binaryName. The previous version was restored."
1173
1385
  }
1174
1386
 
1175
1387
  Write-InstallManifest $script:Version $InstallPath $selected -LCodeOnly:$LCodeOnly
1176
- Write-InstallState $script:Version $InstallPath $selected $Source -LCodeOnly:$LCodeOnly
1388
+ $stateSource = if ($script:ExistingInstall -and $script:ExistingInstall.Uninstaller) { 'inno' } else { $Source }
1389
+ Write-InstallState $script:Version $InstallPath $selected $stateSource -LCodeOnly:$LCodeOnly
1177
1390
 
1178
- # Keep a copy of this script next to luc.exe so that "luc --install" works
1391
+ # Keep a copy of this script next to luc.exe so that "luc install" works
1179
1392
  # even when the archive was built before install.ps1 was bundled in it.
1180
1393
  if (-not $LCodeOnly) {
1181
1394
  try {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@hsusulist/luc",
3
- "version": "0.2.5",
4
- "description": "LUC language installer and launcher for Windows",
3
+ "version": "0.3.1",
4
+ "description": "LUC language installer and launcher",
5
5
  "bin": {
6
6
  "luc": "bin/luc.js"
7
7
  },
8
8
  "files": [
9
9
  "bin",
10
10
  "scripts",
11
+ "src",
11
12
  "install.ps1",
12
13
  "LICENCE",
13
14
  "README.md"
@@ -17,9 +18,7 @@
17
18
  "postpack": "node scripts/postpack.js",
18
19
  "postinstall": "node scripts/postinstall.js"
19
20
  },
20
- "os": [
21
- "win32"
22
- ],
21
+ "os": ["win32", "linux"],
23
22
  "engines": {
24
23
  "node": ">=16"
25
24
  },
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ function buildLinux(packageDir, name) {
8
+ const sourceDir = path.join(packageDir, 'src');
9
+ const files = ['luc_core.c', 'luc_libs.c', 'luc_trans.c'].map((file) =>
10
+ path.join(sourceDir, file)
11
+ );
12
+ for (const file of files) {
13
+ if (!fs.existsSync(file)) {
14
+ throw new Error('Linux source is missing from this npm package: ' + file);
15
+ }
16
+ }
17
+
18
+ const outputDir = path.join(packageDir, 'native');
19
+ const output = path.join(outputDir, name);
20
+ fs.mkdirSync(outputDir, { recursive: true });
21
+
22
+ const compiler = process.env.CC || 'cc';
23
+ const args = ['-O2', '-std=gnu99', '-o', output].concat(files, ['-lm', '-ldl']);
24
+ const result = spawnSync(compiler, args, { stdio: 'inherit' });
25
+ if (result.error) {
26
+ throw new Error('could not start ' + compiler + ' to build LUC: ' + result.error.message);
27
+ }
28
+ if (result.status !== 0) {
29
+ throw new Error(compiler + ' failed to build LUC (exit code ' + result.status + ').');
30
+ }
31
+ return output;
32
+ }
33
+
34
+ module.exports = { buildLinux };
@@ -5,6 +5,12 @@ const os = require('os');
5
5
  const path = require('path');
6
6
  const { spawnSync } = require('child_process');
7
7
 
8
+ if (process.platform === 'linux') {
9
+ const { buildLinux } = require('./build-linux');
10
+ buildLinux(path.join(__dirname, '..'), 'luc');
11
+ process.exit(0);
12
+ }
13
+
8
14
  function findBinary() {
9
15
  const candidates = [];
10
16
  const local = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
@@ -35,7 +41,7 @@ if (process.platform !== 'win32' || findBinary()) {
35
41
 
36
42
  const script = path.join(__dirname, '..', 'install.ps1');
37
43
  if (!fs.existsSync(script)) {
38
- console.warn('luc: install.ps1 is missing; run "luc --install" after installing.');
44
+ console.warn('luc: install.ps1 is missing; run "luc install" after installing.');
39
45
  process.exit(0);
40
46
  }
41
47
 
@@ -47,6 +53,6 @@ const result = spawnSync(
47
53
  );
48
54
 
49
55
  if (result.error || result.status !== 0) {
50
- console.warn('luc: automatic install did not finish. Run "luc --install" to open the installer.');
56
+ console.warn('luc: automatic install did not finish. Run "luc install" to open the installer.');
51
57
  }
52
58
  process.exit(0);
@@ -9,3 +9,17 @@ for (const name of ['install.ps1', 'LICENCE']) {
9
9
  } catch (_) {
10
10
  }
11
11
  }
12
+
13
+ const source = path.join(process.cwd(), 'src');
14
+ const marker = path.join(process.cwd(), '.npm-source-bundle-generated');
15
+ if (fs.existsSync(marker)) {
16
+ fs.rmSync(source, { recursive: true, force: true });
17
+ fs.unlinkSync(marker);
18
+ }
19
+
20
+ const helper = path.join(process.cwd(), 'scripts', 'build-linux.js');
21
+ const helperMarker = path.join(process.cwd(), '.npm-build-linux-generated');
22
+ if (fs.existsSync(helperMarker)) {
23
+ fs.unlinkSync(helper);
24
+ fs.unlinkSync(helperMarker);
25
+ }
@@ -17,3 +17,30 @@ function copy(name) {
17
17
 
18
18
  copy('install.ps1');
19
19
  copy('LICENCE');
20
+
21
+ const source = path.join(root, 'src');
22
+ const target = path.join(here, 'src');
23
+ if (fs.existsSync(target)) {
24
+ throw new Error('refusing to overwrite existing npm/src directory');
25
+ }
26
+ fs.mkdirSync(target);
27
+ for (const name of ['luc_core.c', 'luc_libs.c', 'luc_nums.c', 'luc_trans.c', 'luc.h']) {
28
+ const from = path.join(source, name);
29
+ if (!fs.existsSync(from)) throw new Error('missing release source: ' + from);
30
+ fs.copyFileSync(from, path.join(target, name));
31
+ }
32
+ const linuxEditor = path.join(source, 'lcode-linux', 'lcode.c');
33
+ if (!fs.existsSync(linuxEditor)) throw new Error('missing Linux editor source: ' + linuxEditor);
34
+ fs.mkdirSync(path.join(target, 'lcode-linux'));
35
+ fs.copyFileSync(linuxEditor, path.join(target, 'lcode-linux', 'lcode.c'));
36
+ fs.writeFileSync(path.join(here, '.npm-source-bundle-generated'), 'generated by prepack\n');
37
+
38
+ const helper = path.join(__dirname, 'build-linux.js');
39
+ const packagedHelper = path.join(here, 'scripts', 'build-linux.js');
40
+ if (helper !== packagedHelper) {
41
+ if (fs.existsSync(packagedHelper)) {
42
+ throw new Error('refusing to overwrite existing scripts/build-linux.js');
43
+ }
44
+ fs.copyFileSync(helper, packagedHelper);
45
+ fs.writeFileSync(path.join(here, '.npm-build-linux-generated'), 'generated by prepack\n');
46
+ }