@pixelbyte-software/pixcode 1.51.5 → 1.51.6

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.
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { execFile } from 'node:child_process';
4
+ import { execFile, spawn } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
 
7
7
  import bcrypt from 'bcryptjs';
@@ -9,7 +9,7 @@ import bcrypt from 'bcryptjs';
9
9
  import { appConfigDb, userDb } from '../database/db.js';
10
10
 
11
11
  const CONFIG_KEY = 'platformization';
12
- const execFileAsync = promisify(execFile);
12
+ const execFileAsync = promisify(execFile);
13
13
 
14
14
  export const TEAM_ROLES = {
15
15
  owner: [
@@ -803,9 +803,66 @@ export function exportAuditLog(format = 'json', filters = {}) {
803
803
  return JSON.stringify(entries, null, 2);
804
804
  }
805
805
 
806
- function normalizeAccessMode(mode) {
807
- return ['lan', 'tailscale', 'cloudflare_tunnel', 'custom_domain'].includes(mode) ? mode : 'lan';
808
- }
806
+ function normalizeAccessMode(mode) {
807
+ return ['lan', 'tailscale', 'cloudflare_tunnel', 'custom_domain'].includes(mode) ? mode : 'lan';
808
+ }
809
+
810
+ function resolveTailscaleInstallPlan() {
811
+ const platform = os.platform();
812
+ if (platform === 'darwin') {
813
+ return {
814
+ platform,
815
+ command: 'brew',
816
+ args: ['install', 'tailscale'],
817
+ displayCommand: 'brew install tailscale',
818
+ docsUrl: 'https://tailscale.com/download/mac',
819
+ note: 'If Homebrew is not installed, open the download page and install the macOS app.',
820
+ };
821
+ }
822
+ if (platform === 'win32') {
823
+ return {
824
+ platform,
825
+ command: 'winget',
826
+ args: ['install', '--id', 'Tailscale.Tailscale', '-e', '--silent'],
827
+ displayCommand: 'winget install --id Tailscale.Tailscale -e --silent',
828
+ docsUrl: 'https://tailscale.com/download/windows',
829
+ note: 'If winget is unavailable, install from the Tailscale download page.',
830
+ };
831
+ }
832
+ return {
833
+ platform,
834
+ command: 'sh',
835
+ args: ['-c', 'curl -fsSL https://tailscale.com/install.sh | sh'],
836
+ displayCommand: 'curl -fsSL https://tailscale.com/install.sh | sh',
837
+ docsUrl: 'https://tailscale.com/download/linux',
838
+ note: 'Linux install may require root privileges. If this fails, run the command with sudo in a terminal.',
839
+ };
840
+ }
841
+
842
+ function extractFirstUrl(text = '') {
843
+ return String(text).match(/https?:\/\/[^\s]+/i)?.[0] || null;
844
+ }
845
+
846
+ function runTailscaleCommand(command, args, options = {}) {
847
+ return new Promise((resolve) => {
848
+ const child = spawn(command, args, {
849
+ shell: false,
850
+ windowsHide: true,
851
+ env: process.env,
852
+ ...options,
853
+ });
854
+ let stdout = '';
855
+ let stderr = '';
856
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
857
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
858
+ child.on('error', (error) => {
859
+ resolve({ ok: false, code: null, stdout, stderr, error: error.message });
860
+ });
861
+ child.on('close', (code) => {
862
+ resolve({ ok: code === 0, code, stdout, stderr, error: code === 0 ? null : `${command} exited with code ${code}` });
863
+ });
864
+ });
865
+ }
809
866
 
810
867
  function normalizePublicUrl(value) {
811
868
  const raw = typeof value === 'string' ? value.trim() : '';
@@ -872,8 +929,9 @@ export function getRemoteAccessState() {
872
929
  };
873
930
  }
874
931
 
875
- export async function detectTailscaleStatus() {
876
- try {
932
+ export async function detectTailscaleStatus() {
933
+ const installPlan = resolveTailscaleInstallPlan();
934
+ try {
877
935
  const { stdout } = await execFileAsync('tailscale', ['status', '--json'], { timeout: 5000 });
878
936
  const status = JSON.parse(stdout || '{}');
879
937
  const self = status.Self || {};
@@ -886,7 +944,8 @@ export async function detectTailscaleStatus() {
886
944
  magicDnsName: self.DNSName || null,
887
945
  tailscaleIp: tailscaleIps[0] || null,
888
946
  pixcodeUrl: tailscaleIps[0] ? `http://${tailscaleIps[0]}:${process.env.SERVER_PORT || 3001}` : null,
889
- installUrl: 'https://tailscale.com/download',
947
+ installUrl: 'https://tailscale.com/download',
948
+ installPlan,
890
949
  checkedAt: nowIso(),
891
950
  message: tailscaleIps[0] ? 'Tailscale is ready for private Pixcode access.' : 'Tailscale CLI is installed but no device IP was detected.',
892
951
  };
@@ -900,14 +959,54 @@ export async function detectTailscaleStatus() {
900
959
  magicDnsName: null,
901
960
  tailscaleIp: null,
902
961
  pixcodeUrl: null,
903
- installUrl: 'https://tailscale.com/download',
962
+ installUrl: 'https://tailscale.com/download',
963
+ installPlan,
904
964
  checkedAt: nowIso(),
905
965
  message: isMissing
906
966
  ? 'Tailscale is optional. Use the LAN links now, or install Tailscale from Settings > Access for private team access without a public domain.'
907
967
  : (error?.message || 'Tailscale status could not be read.'),
908
- };
909
- }
910
- }
968
+ };
969
+ }
970
+ }
971
+
972
+ export async function installTailscale(actorId = null) {
973
+ const plan = resolveTailscaleInstallPlan();
974
+ const result = await runTailscaleCommand(plan.command, plan.args);
975
+ const store = readStore();
976
+ addAudit(store, 'remote.access.tailscale.install', actorId, {
977
+ platform: plan.platform,
978
+ ok: result.ok,
979
+ command: plan.displayCommand,
980
+ });
981
+ writeStore(store);
982
+ return {
983
+ ...result,
984
+ plan,
985
+ message: result.ok
986
+ ? 'Tailscale install command completed. Run login/connect next.'
987
+ : `Install command failed. ${plan.note}`,
988
+ };
989
+ }
990
+
991
+ export async function loginTailscale(actorId = null) {
992
+ const result = await runTailscaleCommand('tailscale', ['up']);
993
+ const combinedOutput = `${result.stdout || ''}\n${result.stderr || ''}`;
994
+ const authUrl = extractFirstUrl(combinedOutput);
995
+ const store = readStore();
996
+ addAudit(store, 'remote.access.tailscale.login', actorId, {
997
+ ok: result.ok,
998
+ authUrl: Boolean(authUrl),
999
+ });
1000
+ writeStore(store);
1001
+ return {
1002
+ ...result,
1003
+ authUrl,
1004
+ message: result.ok
1005
+ ? 'Tailscale is connected.'
1006
+ : (authUrl ? 'Open the login URL to finish connecting this device.' : 'Tailscale login command failed.'),
1007
+ tailscale: await detectTailscaleStatus(),
1008
+ };
1009
+ }
911
1010
 
912
1011
  export async function checkRemoteAccessHealth(input = {}, actorId = null) {
913
1012
  const url = normalizePublicUrl(input.url || input.remoteUrl || '');