@biklitime/biklimaster 1.1.8 → 1.1.9

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/bin/bikli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function installedBikliPath() {
9
+ const candidates = [
10
+ path.join(process.env.ProgramW6432 || 'C:\\Program Files', 'Bikli', 'Bikli.exe'),
11
+ path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli', 'Bikli.exe'),
12
+ path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli', 'Bikli.exe'),
13
+ path.join(process.env.LOCALAPPDATA || 'C:\\Users\\Default\\AppData\\Local', 'Programs', 'Bikli', 'Bikli.exe'),
14
+ path.join(process.env.USERPROFILE || 'C:\\Users\\Default', 'AppData', 'Local', 'Programs', 'Bikli', 'Bikli.exe')
15
+ ];
16
+ const found = candidates.find(candidate => fs.existsSync(candidate));
17
+ if (found) return found;
18
+ try {
19
+ const whereResult = spawnSync('where.exe', ['bikli.exe'], { encoding: 'utf8', windowsHide: true });
20
+ if (whereResult.status === 0 && whereResult.stdout) {
21
+ const firstLine = whereResult.stdout.trim().split(/\r?\n/)[0];
22
+ if (firstLine && fs.existsSync(firstLine)) return firstLine;
23
+ }
24
+ } catch {}
25
+ return '';
26
+ }
27
+
28
+ const bikliExe = installedBikliPath();
29
+ if (!bikliExe) {
30
+ console.error('Bikli CLI is not installed. Run "biklimaster install" to install it.');
31
+ process.exit(1);
32
+ }
33
+
34
+ const result = spawnSync(bikliExe, process.argv.slice(2), {
35
+ stdio: 'inherit',
36
+ windowsHide: false
37
+ });
38
+
39
+ if (result.error) {
40
+ console.error(`Failed to run Bikli CLI: ${result.error.message}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ process.exit(result.status === null ? 1 : result.status);
@@ -33,14 +33,34 @@ const expectedHashes = {
33
33
  };
34
34
 
35
35
  const resultArgument = process.argv.find(argument => argument.startsWith('--result-file='));
36
- const resultFile = resultArgument ? resultArgument.slice('--result-file='.length) : '';
36
+ const resultFile = resultArgument ? resultArgument.slice('--result-file='.length).replace(/^"|"$/g, '') : '';
37
37
  if (resultFile) {
38
- const writeResult = (...items) => fs.appendFileSync(resultFile, util.format(...items) + os.EOL, 'utf8');
38
+ const writeResult = (...items) => {
39
+ try {
40
+ fs.appendFileSync(resultFile, util.format(...items) + os.EOL, 'utf8');
41
+ } catch {}
42
+ };
39
43
  console.log = writeResult;
40
44
  console.error = writeResult;
41
45
  console.warn = writeResult;
42
46
  }
43
47
 
48
+ const envFileArgument = process.argv.find(argument => argument.startsWith('--env-file='));
49
+ const envFile = envFileArgument ? envFileArgument.slice('--env-file='.length).replace(/^"|"$/g, '') : '';
50
+ if (envFile && fs.existsSync(envFile)) {
51
+ try {
52
+ const passedEnv = JSON.parse(fs.readFileSync(envFile, 'utf8'));
53
+ for (const [key, value] of Object.entries(passedEnv)) {
54
+ if (value !== undefined && value !== null) {
55
+ process.env[key] = String(value);
56
+ }
57
+ }
58
+ } catch {
59
+ // Ignore invalid env file
60
+ }
61
+ try { fs.rmSync(envFile, { force: true }); } catch {}
62
+ }
63
+
44
64
  function fail(message, exitCode = 1) {
45
65
  const error = new Error(message);
46
66
  error.exitCode = exitCode;
@@ -73,7 +93,7 @@ function isAdministrator() {
73
93
  `$principal=New-Object Security.Principal.WindowsPrincipal($identity)`,
74
94
  `Write-Output $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)`
75
95
  ].join(';');
76
- const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], {
96
+ const result = run(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
77
97
  allowFailure: true
78
98
  });
79
99
  return result.status === 0 && /^true$/i.test(result.stdout.trim());
@@ -87,13 +107,31 @@ function elevateAndRun(command) {
87
107
  if (process.argv.includes('--elevated')) fail('Windows did not grant administrator rights.');
88
108
  console.log('Bikli Master needs administrator access. Approve the Windows UAC prompt...');
89
109
  const logPath = path.join(os.tmpdir(), `biklimaster-elevated-${randomUUID()}.log`);
110
+ const envPath = path.join(os.tmpdir(), `biklimaster-env-${randomUUID()}.json`);
111
+
112
+ const envToForward = {};
113
+ for (const [k, v] of Object.entries(process.env)) {
114
+ if (k.startsWith('BIKLIMASTER_') && v) envToForward[k] = v;
115
+ }
116
+ const hasEnv = Object.keys(envToForward).length > 0;
117
+ if (hasEnv) {
118
+ fs.writeFileSync(envPath, JSON.stringify(envToForward), { encoding: 'utf8', mode: 0o600 });
119
+ }
120
+
90
121
  const script = [
91
122
  `$node=${powershellLiteral(process.execPath)}`,
92
123
  `$target=${powershellLiteral(path.resolve(__filename))}`,
93
124
  `$result=${powershellLiteral(logPath)}`,
94
- `$arguments='"'+$target+'" ${command} --elevated "--result-file='+$result+'"'`,
95
- `$process=Start-Process -FilePath $node -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden -Wait -PassThru`,
96
- `exit $process.ExitCode`
125
+ `$envFile=${powershellLiteral(hasEnv ? envPath : '')}`,
126
+ `$arguments='"'+$target+'" ${command} --elevated "--result-file='+$result+'"'+(if($envFile){' "--env-file='+$envFile+'"'}else{''})`,
127
+ `try {`,
128
+ ` $process=Start-Process -FilePath $node -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden -Wait -PassThru`,
129
+ ` if($null -eq $process -or $null -eq $process.ExitCode){exit 1}`,
130
+ ` exit $process.ExitCode`,
131
+ `} catch {`,
132
+ ` Write-Error $_.Exception.Message`,
133
+ ` exit 1`,
134
+ `}`
97
135
  ].join(';');
98
136
  const result = run(powershell, [
99
137
  '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script
@@ -103,6 +141,9 @@ function elevateAndRun(command) {
103
141
  elevatedOutput = fs.readFileSync(logPath, 'utf8').trim();
104
142
  fs.rmSync(logPath, { force: true });
105
143
  }
144
+ if (hasEnv && fs.existsSync(envPath)) {
145
+ fs.rmSync(envPath, { force: true });
146
+ }
106
147
  if (result.status !== 0) {
107
148
  const details = [elevatedOutput, result.stdout, result.stderr].filter(Boolean).join(os.EOL).trim();
108
149
  fail(`Administrator elevation was cancelled or installation failed.${details ? `\n${details}` : ''}`);
@@ -126,17 +167,29 @@ function verifyPayload() {
126
167
 
127
168
  function installedBikliPath() {
128
169
  const candidates = [
170
+ path.join(process.env.ProgramW6432 || 'C:\\Program Files', 'Bikli', 'Bikli.exe'),
129
171
  path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli', 'Bikli.exe'),
130
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli', 'Bikli.exe')
172
+ path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli', 'Bikli.exe'),
173
+ path.join(process.env.LOCALAPPDATA || 'C:\\Users\\Default\\AppData\\Local', 'Programs', 'Bikli', 'Bikli.exe'),
174
+ path.join(process.env.USERPROFILE || 'C:\\Users\\Default', 'AppData', 'Local', 'Programs', 'Bikli', 'Bikli.exe')
131
175
  ];
132
- return candidates.find(candidate => fs.existsSync(candidate)) || '';
176
+ const found = candidates.find(candidate => fs.existsSync(candidate));
177
+ if (found) return found;
178
+ try {
179
+ const whereResult = spawnSync('where.exe', ['bikli.exe'], { encoding: 'utf8', windowsHide: true });
180
+ if (whereResult.status === 0 && whereResult.stdout) {
181
+ const firstLine = whereResult.stdout.trim().split(/\r?\n/)[0];
182
+ if (firstLine && fs.existsSync(firstLine)) return firstLine;
183
+ }
184
+ } catch {}
185
+ return '';
133
186
  }
134
187
 
135
188
  function fileVersion(file) {
136
189
  if (!file) return '';
137
190
  const escaped = file.replace(/'/g, "''");
138
191
  const script = `(Get-Item -LiteralPath '${escaped}').VersionInfo.FileVersion`;
139
- const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], {
192
+ const result = run(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
140
193
  allowFailure: true
141
194
  });
142
195
  return result.status === 0 ? result.stdout.trim() : '';
@@ -150,7 +203,16 @@ function installBikli() {
150
203
  }
151
204
  console.log(`Installing Bikli CLI ${expectedBikliVersion} silently...`);
152
205
  run(bikliInstaller, ['/S'], { cwd: payloadDirectory });
153
- const installed = installedBikliPath();
206
+ let installed = installedBikliPath();
207
+ if (!installed) {
208
+ const start = Date.now();
209
+ const waitBuffer = new Int32Array(new SharedArrayBuffer(4));
210
+ while (Date.now() - start < 10000) {
211
+ installed = installedBikliPath();
212
+ if (installed) break;
213
+ Atomics.wait(waitBuffer, 0, 0, 500);
214
+ }
215
+ }
154
216
  if (!installed) fail('Bikli CLI installer completed but Bikli.exe was not found.', 5);
155
217
  console.log(`Bikli CLI installed: ${fileVersion(installed) || 'version unknown'}`);
156
218
  }
@@ -200,15 +262,23 @@ function generatedAccountPassword() {
200
262
  }
201
263
 
202
264
  function configuredAdministratorPassword() {
203
- if (!fs.existsSync(packageConfigFile)) fail('Missing package configuration file: config.json');
204
- const config = JSON.parse(fs.readFileSync(packageConfigFile, 'utf8'));
205
- return typeof config.administratorPassword === 'string' ? config.administratorPassword : '';
265
+ if (!fs.existsSync(packageConfigFile)) return '';
266
+ try {
267
+ const config = JSON.parse(fs.readFileSync(packageConfigFile, 'utf8'));
268
+ return typeof config.administratorPassword === 'string' ? config.administratorPassword.trim() : '';
269
+ } catch (error) {
270
+ fail(`Could not parse config.json: ${error.message}`);
271
+ }
206
272
  }
207
273
 
208
274
  function configuredBikliKey() {
209
275
  if (!fs.existsSync(packageConfigFile)) return '';
210
- const config = JSON.parse(fs.readFileSync(packageConfigFile, 'utf8'));
211
- return typeof config.bikliKey === 'string' ? config.bikliKey.trim() : '';
276
+ try {
277
+ const config = JSON.parse(fs.readFileSync(packageConfigFile, 'utf8'));
278
+ return typeof config.bikliKey === 'string' ? config.bikliKey.trim() : '';
279
+ } catch (error) {
280
+ fail(`Could not parse config.json: ${error.message}`);
281
+ }
212
282
  }
213
283
 
214
284
  function setupBikliKey() {
@@ -246,16 +316,30 @@ function saveAccountCredentials(username, password) {
246
316
  ]);
247
317
  }
248
318
 
319
+ function savedAccountUsername() {
320
+ if (!fs.existsSync(credentialsFile)) return '';
321
+ try {
322
+ const credentials = JSON.parse(fs.readFileSync(credentialsFile, 'utf8'));
323
+ return typeof credentials.username === 'string' ? credentials.username.trim() : '';
324
+ } catch {
325
+ return '';
326
+ }
327
+ }
328
+
249
329
  function showAccountCredentials() {
250
330
  requireWindows();
251
331
  if (!isAdministrator()) return elevateAndRun('credentials');
252
332
  if (!fs.existsSync(credentialsFile)) {
253
333
  fail('No saved password is available. The built-in Administrator account may already have been enabled.');
254
334
  }
255
- const credentials = JSON.parse(fs.readFileSync(credentialsFile, 'utf8'));
256
- console.log(`Username: ${credentials.username}`);
257
- console.log(`Password: ${credentials.password}`);
258
- console.log(`Stored for Administrators only: ${credentialsFile}`);
335
+ try {
336
+ const credentials = JSON.parse(fs.readFileSync(credentialsFile, 'utf8'));
337
+ console.log(`Username: ${credentials.username || 'unknown'}`);
338
+ console.log(`Password: ${credentials.password || 'unknown'}`);
339
+ console.log(`Stored for Administrators only: ${credentialsFile}`);
340
+ } catch (error) {
341
+ fail(`Could not read credentials file: ${error.message}`);
342
+ }
259
343
  }
260
344
 
261
345
  function createRdpAdministrator() {
@@ -264,29 +348,39 @@ function createRdpAdministrator() {
264
348
 
265
349
  const requestedPassword = process.env.BIKLIMASTER_USER_PASSWORD || configuredAdministratorPassword();
266
350
  const accountPassword = requestedPassword || generatedAccountPassword();
351
+ const savedUser = savedAccountUsername();
267
352
  const script = [
268
353
  `$ErrorActionPreference='Stop'`,
269
354
  `$password=$env:BIKLIMASTER_ACCOUNT_PASSWORD`,
355
+ `$savedUser=$env:BIKLIMASTER_SAVED_USERNAME`,
270
356
  `$secure=ConvertTo-SecureString $password -AsPlainText -Force`,
271
357
  `$groupSids=@(${powershellLiteral(administratorsGroupSid)},${powershellLiteral(remoteDesktopUsersGroupSid)})`,
272
358
  `$builtIn=Get-LocalUser | Where-Object {$_.SID.Value -match '-500$'} | Select-Object -First 1`,
273
359
  `if($null -eq $builtIn){throw 'Built-in Administrator account (RID 500) was not found'}`,
274
360
  `$builtInWasDisabled=-not $builtIn.Enabled`,
361
+ `$managedUser=$null`,
362
+ `if($savedUser){$managedUser=Get-LocalUser -Name $savedUser -ErrorAction SilentlyContinue}`,
363
+ `if($null -eq $managedUser){$managedUser=Get-LocalUser | Where-Object {$_.Description -eq 'Local administrator created by Bikli Master'} | Select-Object -First 1}`,
275
364
  `$target=$null;$action='';$createdNew=$false;$enabledBuiltIn=$false;$passwordChanged=$false`,
276
- `if($builtInWasDisabled){Set-LocalUser -Name $builtIn.Name -Password $secure;Enable-LocalUser -Name $builtIn.Name;$target=Get-LocalUser -SID $builtIn.SID;$enabledBuiltIn=$true;$passwordChanged=$true;$action='enabled-builtin'}else{$admin=Get-LocalUser -Name 'admin' -ErrorAction SilentlyContinue;if($null -eq $admin){New-LocalUser -Name 'admin' -Password $secure -FullName 'admin' -Description 'Local administrator created by Bikli Master' -PasswordNeverExpires | Out-Null;$target=Get-LocalUser -Name 'admin';$createdNew=$true;$passwordChanged=$true;$action='created-admin'}else{$existingUser=Get-LocalUser -Name 'user' -ErrorAction SilentlyContinue;if($null -eq $existingUser){New-LocalUser -Name 'user' -Password $secure -FullName 'user' -Description 'Local administrator created by Bikli Master' -PasswordNeverExpires | Out-Null;$target=Get-LocalUser -Name 'user';$createdNew=$true;$passwordChanged=$true;$action='created-user'}else{$target=$existingUser;$action='exists-user'}}}`,
277
- `if(-not $target.Enabled){Enable-LocalUser -Name $target.Name;$target=Get-LocalUser -SID $target.SID}`,
278
- `foreach($groupSid in $groupSids){$group=Get-LocalGroup -SID $groupSid;$member=Get-LocalGroupMember -Group $group.Name | Where-Object {$_.SID.Value -eq $target.SID.Value};if($null -eq $member){Add-LocalGroupMember -Group $group.Name -Member $target}}`,
365
+ `if($builtInWasDisabled){Set-LocalUser -Name $builtIn.Name -Password $secure;Unlock-LocalUser -Name $builtIn.Name -ErrorAction SilentlyContinue;Enable-LocalUser -Name $builtIn.Name;$target=Get-LocalUser -SID $builtIn.SID;$enabledBuiltIn=$true;$passwordChanged=$true;$action='enabled-builtin'}elseif($null -ne $managedUser){$target=$managedUser;if($password){Set-LocalUser -Name $target.Name -Password $secure -ErrorAction SilentlyContinue;$passwordChanged=$true};$action='reused-managed'}else{$admin=Get-LocalUser -Name 'admin' -ErrorAction SilentlyContinue;if($null -eq $admin){New-LocalUser -Name 'admin' -Password $secure -FullName 'admin' -Description 'Local administrator created by Bikli Master' -PasswordNeverExpires | Out-Null;$target=Get-LocalUser -Name 'admin';$createdNew=$true;$passwordChanged=$true;$action='created-admin'}else{$existingUser=Get-LocalUser -Name 'user' -ErrorAction SilentlyContinue;if($null -eq $existingUser){New-LocalUser -Name 'user' -Password $secure -FullName 'user' -Description 'Local administrator created by Bikli Master' -PasswordNeverExpires | Out-Null;$target=Get-LocalUser -Name 'user';$createdNew=$true;$passwordChanged=$true;$action='created-user'}else{$target=$existingUser;$action='exists-user'}}}`,
366
+ `if(-not $target.Enabled){Unlock-LocalUser -Name $target.Name -ErrorAction SilentlyContinue;Enable-LocalUser -Name $target.Name;$target=Get-LocalUser -SID $target.SID}`,
367
+ `foreach($groupSid in $groupSids){$group=Get-LocalGroup -SID $groupSid;$member=Get-LocalGroupMember -Group $group.Name -ErrorAction SilentlyContinue | Where-Object {$_.SID.Value -eq $target.SID.Value};if($null -eq $member){Add-LocalGroupMember -Group $group.Name -Member $target.Name}}`,
279
368
  `$verified=@()`,
280
- `foreach($groupSid in $groupSids){$group=Get-LocalGroup -SID $groupSid;$member=Get-LocalGroupMember -Group $group.Name | Where-Object {$_.SID.Value -eq $target.SID.Value};if($null -eq $member){throw ('Account is not a member of '+$group.Name)};$verified+=$group.Name}`,
369
+ `foreach($groupSid in $groupSids){$group=Get-LocalGroup -SID $groupSid;$member=Get-LocalGroupMember -Group $group.Name -ErrorAction SilentlyContinue | Where-Object {$_.SID.Value -eq $target.SID.Value};if($null -eq $member){throw ('Account is not a member of '+$group.Name)};$verified+=$group.Name}`,
281
370
  `$userListKey='HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\SpecialAccounts\\UserList'`,
282
- `$hideEntry=Get-ItemProperty -Path $userListKey -Name $target.Name -ErrorAction SilentlyContinue`,
283
- `if($null -eq $hideEntry){New-Item -Path $userListKey -Force | Out-Null;New-ItemProperty -Path $userListKey -Name $target.Name -PropertyType DWord -Value 0 -Force | Out-Null;if((Get-ItemPropertyValue -Path $userListKey -Name $target.Name) -ne 0){throw ('Could not hide '+$target.Name+' from the sign-in screen')};$alreadyHidden=$false}else{$alreadyHidden=$true}`,
371
+ `$specialAccountsKey='HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\SpecialAccounts'`,
372
+ `if(-not(Test-Path $specialAccountsKey)){New-Item -Path $specialAccountsKey -Force | Out-Null}`,
373
+ `if(-not(Test-Path $userListKey)){New-Item -Path $userListKey -Force | Out-Null}`,
374
+ `$hideProp=Get-ItemProperty -Path $userListKey -Name $target.Name -ErrorAction SilentlyContinue`,
375
+ `if($null -eq $hideProp -or $null -eq $hideProp.($target.Name)){Set-ItemProperty -Path $userListKey -Name $target.Name -Value 0 -Type DWord -Force | Out-Null;$alreadyHidden=$false}elseif($hideProp.($target.Name) -ne 0){Set-ItemProperty -Path $userListKey -Name $target.Name -Value 0 -Type DWord -Force | Out-Null;$alreadyHidden=$false}else{$alreadyHidden=$true}`,
376
+ `if((Get-ItemProperty -Path $userListKey -Name $target.Name -ErrorAction SilentlyContinue).($target.Name) -ne 0){throw ('Could not hide '+$target.Name+' from the sign-in screen')}`,
284
377
  `[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`
285
378
  ].join(';');
286
- const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], {
379
+ const result = run(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
287
380
  env: {
288
381
  ...process.env,
289
- BIKLIMASTER_ACCOUNT_PASSWORD: accountPassword
382
+ BIKLIMASTER_ACCOUNT_PASSWORD: accountPassword,
383
+ BIKLIMASTER_SAVED_USERNAME: savedUser
290
384
  }
291
385
  });
292
386
  const account = JSON.parse(result.stdout.trim());
@@ -299,7 +393,8 @@ function createRdpAdministrator() {
299
393
  const message = {
300
394
  'enabled-builtin': `Built-in Administrator was disabled; set the password and enabled it: ${account.Name}`,
301
395
  'created-admin': `Built-in Administrator is already enabled and left untouched; created hidden admin account: ${account.Name}`,
302
- 'created-user': `Built-in Administrator and admin already exist and were left untouched; created hidden account: ${account.Name}`
396
+ 'created-user': `Built-in Administrator and admin already exist and were left untouched; created hidden account: ${account.Name}`,
397
+ 'reused-managed': `Verified existing Bikli Master administrator account: ${account.Name}`
303
398
  }[account.Action] || `Configured account: ${account.Name}`;
304
399
  console.log(message);
305
400
  console.log(`Password: ${accountPassword}`);
@@ -338,7 +433,7 @@ function status() {
338
433
  const wrapper = runWrapper(['status'], { allowFailure: true });
339
434
  if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
340
435
  if (wrapper.stderr.trim()) console.error(wrapper.stderr.trim());
341
- process.exitCode = bikli && wrapper.status === 0 ? 0 : 2;
436
+ process.exitCode = Boolean(bikli) && wrapper.status === 0 ? 0 : 2;
342
437
  }
343
438
 
344
439
  function selfTest() {
@@ -390,7 +485,7 @@ function main() {
390
485
  if (command === 'setup-key') return setupBikliKey();
391
486
  if (command === 'credentials') return showAccountCredentials();
392
487
  if (command === 'status') return status();
393
- if (command === 'self-test') return selfTest();
488
+ if (command === 'self-test' || command === 'test') return selfTest();
394
489
  if (command === 'elevation-self-test') return elevateAndRun('self-test');
395
490
  if (command === 'wrapper') {
396
491
  const result = runWrapper(process.argv.slice(3), { inherit: true, allowFailure: true });
@@ -17,13 +17,16 @@ try {
17
17
  const runningAsSea = Boolean(seaApi && typeof seaApi.isSea === 'function' && seaApi.isSea());
18
18
 
19
19
  const resultFileArgument = process.argv.find(argument => argument.startsWith('--result-file='));
20
- const resultFile = resultFileArgument ? resultFileArgument.slice('--result-file='.length) : '';
20
+ const resultFile = resultFileArgument ? resultFileArgument.slice('--result-file='.length).replace(/^"|"$/g, '') : '';
21
21
  if (resultFile) {
22
22
  const writeResult = (...items) => {
23
- fs.appendFileSync(resultFile, util.format(...items) + os.EOL, 'utf8');
23
+ try {
24
+ fs.appendFileSync(resultFile, util.format(...items) + os.EOL, 'utf8');
25
+ } catch {}
24
26
  };
25
27
  console.log = writeResult;
26
28
  console.error = writeResult;
29
+ console.warn = writeResult;
27
30
  }
28
31
 
29
32
  const packageRoot = path.resolve(__dirname, '..');
@@ -79,6 +82,7 @@ function run(executable, args, options = {}) {
79
82
  cwd: options.cwd || packageRoot,
80
83
  encoding: 'utf8',
81
84
  windowsHide: true,
85
+ env: options.env || process.env,
82
86
  stdio: options.inherit ? 'inherit' : 'pipe'
83
87
  });
84
88
  if (result.error) fail(`Could not run ${path.basename(executable)}: ${result.error.message}`);
@@ -99,7 +103,7 @@ function isAdministrator() {
99
103
  `$principal=New-Object Security.Principal.WindowsPrincipal($identity)`,
100
104
  `Write-Output $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)`
101
105
  ].join(';');
102
- const result = run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script], {
106
+ const result = run(powershellPath, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
103
107
  allowFailure: true
104
108
  });
105
109
  return result.status === 0 && /^true$/i.test(result.stdout.trim());
@@ -124,8 +128,14 @@ function elevateAndRun(command) {
124
128
  runningAsSea
125
129
  ? `$arguments='${command} --elevated "--result-file='+$result+'"'`
126
130
  : `$arguments='"'+$target+'" ${command} --elevated "--result-file='+$result+'"'`,
127
- `$process=Start-Process -FilePath $node -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden -Wait -PassThru`,
128
- `exit $process.ExitCode`
131
+ `try {`,
132
+ ` $process=Start-Process -FilePath $node -ArgumentList $arguments -Verb RunAs -WindowStyle Hidden -Wait -PassThru`,
133
+ ` if($null -eq $process -or $null -eq $process.ExitCode){exit 1}`,
134
+ ` exit $process.ExitCode`,
135
+ `} catch {`,
136
+ ` Write-Error $_.Exception.Message`,
137
+ ` exit 1`,
138
+ `}`
129
139
  ].join(';');
130
140
  const result = run(powershellPath, [
131
141
  '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', elevationScript
@@ -148,7 +158,7 @@ function getTermsrvVersion() {
148
158
  `$v=[Diagnostics.FileVersionInfo]::GetVersionInfo($env:SystemRoot+'\\System32\\termsrv.dll')`,
149
159
  `Write-Output ($v.FileMajorPart.ToString()+'.'+$v.FileMinorPart+'.'+$v.FileBuildPart+'.'+$v.FilePrivatePart)`
150
160
  ].join(';');
151
- const result = run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script]);
161
+ const result = run(powershellPath, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script]);
152
162
  const version = result.stdout.trim();
153
163
  if (!/^\d+\.\d+\.\d+\.\d+$/.test(version)) fail(`Could not determine the Terminal Services version: ${version}`);
154
164
  return version;
@@ -164,7 +174,7 @@ function queryRegistryValue(key, name) {
164
174
  const result = run(path.join(system32, 'reg.exe'), ['query', key, '/v', name, '/reg:64'], { allowFailure: true });
165
175
  if (result.status !== 0) return null;
166
176
  const match = result.stdout.match(new RegExp(`^\\s*${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+REG_\\w+\\s+(.+)$`, 'im'));
167
- return match ? match[1].trim() : null;
177
+ return match ? match[1].trim().replace(/^"(.*)"$/, '$1') : null;
168
178
  }
169
179
 
170
180
  function queryDword(key, name) {
@@ -176,8 +186,10 @@ function queryDword(key, name) {
176
186
  }
177
187
 
178
188
  function expandWindowsEnvironment(value) {
189
+ if (!value) return '';
190
+ const unquoted = value.replace(/^"(.*)"$/, '$1').trim();
179
191
  const environment = new Map(Object.entries(process.env).map(([key, item]) => [key.toLowerCase(), item]));
180
- return value.replace(/%([^%]+)%/g, (whole, name) => environment.get(name.toLowerCase()) || whole);
192
+ return unquoted.replace(/%([^%]+)%/g, (whole, name) => environment.get(name.toLowerCase()) || whole);
181
193
  }
182
194
 
183
195
  function detectInstallation(version) {
@@ -196,6 +208,7 @@ function detectInstallation(version) {
196
208
 
197
209
  function applyRequestedSettings() {
198
210
  let changed = false;
211
+ run(path.join(system32, 'sc.exe'), ['config', 'TermService', 'start=', 'auto'], { allowFailure: true });
199
212
  for (const setting of requestedSettings) {
200
213
  const oldValue = queryDword(setting.key, setting.name);
201
214
  if (oldValue !== setting.value) {
@@ -213,9 +226,10 @@ function ensureFirewallRules() {
213
226
  const script = [
214
227
  `$ErrorActionPreference='Stop'`,
215
228
  `$rules=@(@{Name='BikliWrapper-RDP-TCP';Protocol='TCP'},@{Name='BikliWrapper-RDP-UDP';Protocol='UDP'})`,
216
- `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}`
229
+ `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}`,
230
+ `Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue | Out-Null`
217
231
  ].join(';');
218
- run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script]);
232
+ run(powershellPath, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script]);
219
233
  }
220
234
 
221
235
  function runInstaller(argument) {
@@ -288,25 +302,40 @@ function install() {
288
302
  if (!installation.installed) {
289
303
  console.log('Installing RDP Wrapper silently...');
290
304
  runInstaller('-i');
291
- } else if (!installation.installedSupported) {
292
- console.log('Updating compatibility data silently...');
293
- const backupPath = `${installation.installedIniPath}.bikli-backup`;
294
- if (fs.existsSync(installation.installedIniPath)) fs.copyFileSync(installation.installedIniPath, backupPath);
295
- fs.copyFileSync(bundledIniPath, installation.installedIniPath);
296
- try {
297
- runInstaller('-r');
298
- } catch (error) {
299
- if (fs.existsSync(backupPath)) {
300
- fs.copyFileSync(backupPath, installation.installedIniPath);
305
+ installation = detectInstallation(version);
306
+ const targetIni = installation.installedIniPath ||
307
+ path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper', 'rdpwrap.ini');
308
+ if (!installation.installedSupported || !fs.existsSync(targetIni)) {
309
+ if (fs.existsSync(path.dirname(targetIni))) {
310
+ fs.copyFileSync(bundledIniPath, targetIni);
301
311
  runInstaller('-r');
302
312
  }
303
- throw error;
304
313
  }
305
- } else if (settingsChanged || !serviceIsRunning()) {
306
- console.log('Applying defaults and restarting Remote Desktop Services...');
307
- runInstaller('-r');
308
314
  } else {
309
- console.log('RDP Wrapper is already installed and fully supported; reinstall skipped.');
315
+ const iniNeedsUpdate = installation.installed && installation.installedIniPath &&
316
+ fs.existsSync(installation.installedIniPath) &&
317
+ fs.readFileSync(bundledIniPath, 'utf8') !== fs.readFileSync(installation.installedIniPath, 'utf8');
318
+
319
+ if (!installation.installedSupported || iniNeedsUpdate) {
320
+ console.log('Updating compatibility data silently...');
321
+ const backupPath = `${installation.installedIniPath}.bikli-backup`;
322
+ if (fs.existsSync(installation.installedIniPath)) fs.copyFileSync(installation.installedIniPath, backupPath);
323
+ fs.copyFileSync(bundledIniPath, installation.installedIniPath);
324
+ try {
325
+ runInstaller('-r');
326
+ } catch (error) {
327
+ if (fs.existsSync(backupPath)) {
328
+ fs.copyFileSync(backupPath, installation.installedIniPath);
329
+ runInstaller('-r');
330
+ }
331
+ throw error;
332
+ }
333
+ } else if (settingsChanged || !serviceIsRunning()) {
334
+ console.log('Applying defaults and restarting Remote Desktop Services...');
335
+ runInstaller('-r');
336
+ } else {
337
+ console.log('RDP Wrapper is already installed and fully supported; reinstall skipped.');
338
+ }
310
339
  }
311
340
 
312
341
  waitForServiceAndListener();
@@ -369,7 +398,7 @@ function main() {
369
398
  status.listenerListening && status.defaultsApplied ? 0 : 2;
370
399
  return;
371
400
  }
372
- if (command === 'self-test') return selfTest();
401
+ if (command === 'self-test' || command === 'test') return selfTest();
373
402
  if (command === 'elevation-self-test') return elevateAndRun('self-test');
374
403
  if (command === 'help' || command === '--help' || command === '-h') return showHelp();
375
404
  fail(`Unknown command: ${command}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biklitime/biklimaster",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Internal Windows installer for Bikli CLI and Bikli Wrapper",
5
5
  "license": "BSD-3-Clause",
6
6
  "publishConfig": {
@@ -11,7 +11,8 @@
11
11
  ],
12
12
  "bin": {
13
13
  "biklimaster": "bin/biklimaster.js",
14
- "bikliwrapper": "lib/bikliwrapper.js"
14
+ "bikliwrapper": "lib/bikliwrapper.js",
15
+ "bikli": "bin/bikli.js"
15
16
  },
16
17
  "files": [
17
18
  "bin",