conduit-mobile 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from 'fs';
3
+ import { get } from 'https';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+
7
+ const CACHE_DIR = join(homedir(), '.conduit');
8
+ const BIN_NAME = process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
9
+ const BIN_PATH = join(CACHE_DIR, BIN_NAME);
10
+
11
+ // ── Locate cloudflared ────────────────────────────────────────────────────────
12
+
13
+ function findOnPath() {
14
+ try {
15
+ execSync(process.platform === 'win32' ? 'where cloudflared' : 'which cloudflared',
16
+ { stdio: 'ignore' });
17
+ return 'cloudflared';
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function getDownloadInfo() {
24
+ const cpu = process.arch === 'arm64' ? 'arm64' : 'amd64';
25
+ switch (process.platform) {
26
+ case 'win32':
27
+ return { url: `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-${cpu}.exe`, tgz: false };
28
+ case 'darwin':
29
+ return { url: `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cpu}.tgz`, tgz: true };
30
+ default:
31
+ return { url: `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cpu}`, tgz: false };
32
+ }
33
+ }
34
+
35
+ // Follow redirects and return the response stream
36
+ function httpGet(url) {
37
+ return new Promise((resolve, reject) => {
38
+ get(url, (res) => {
39
+ if (res.statusCode === 301 || res.statusCode === 302) {
40
+ httpGet(res.headers.location).then(resolve, reject);
41
+ return;
42
+ }
43
+ resolve(res);
44
+ }).on('error', reject);
45
+ });
46
+ }
47
+
48
+ function downloadTo(url, dest) {
49
+ return new Promise(async (resolve, reject) => {
50
+ try {
51
+ const res = await httpGet(url);
52
+ const file = createWriteStream(dest);
53
+ res.pipe(file);
54
+ file.on('finish', () => file.close(resolve));
55
+ file.on('error', reject);
56
+ } catch (err) {
57
+ reject(err);
58
+ }
59
+ });
60
+ }
61
+
62
+ async function downloadCloudflared() {
63
+ if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
64
+
65
+ const { url, tgz } = getDownloadInfo();
66
+ process.stdout.write(' Downloading cloudflared... ');
67
+
68
+ try {
69
+ if (tgz) {
70
+ const tmp = BIN_PATH + '.tgz';
71
+ await downloadTo(url, tmp);
72
+ await new Promise((resolve, reject) => {
73
+ const child = spawn('tar', ['-xzf', tmp, '-C', CACHE_DIR], { stdio: 'ignore' });
74
+ child.on('close', (code) => {
75
+ try { unlinkSync(tmp); } catch {}
76
+ code === 0 ? resolve() : reject(new Error('tar failed'));
77
+ });
78
+ });
79
+ } else {
80
+ await downloadTo(url, BIN_PATH);
81
+ }
82
+
83
+ if (process.platform !== 'win32') chmodSync(BIN_PATH, 0o755);
84
+ console.log('done.');
85
+ return BIN_PATH;
86
+ } catch {
87
+ try { if (existsSync(BIN_PATH)) unlinkSync(BIN_PATH); } catch {}
88
+ console.log('failed.');
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async function resolveCloudflared() {
94
+ const onPath = findOnPath();
95
+ if (onPath) return onPath;
96
+ if (existsSync(BIN_PATH)) return BIN_PATH;
97
+ return downloadCloudflared();
98
+ }
99
+
100
+ // ── Tunnel ────────────────────────────────────────────────────────────────────
101
+
102
+ export async function startTunnel(port) {
103
+ const bin = await resolveCloudflared();
104
+ if (!bin) return null;
105
+
106
+ return new Promise((resolve) => {
107
+ let child;
108
+ try {
109
+ child = spawn(bin, ['tunnel', '--url', `http://localhost:${port}`], {
110
+ stdio: ['ignore', 'pipe', 'pipe'],
111
+ });
112
+ } catch {
113
+ resolve(null);
114
+ return;
115
+ }
116
+
117
+ const timeout = setTimeout(() => resolve(null), 15000);
118
+
119
+ function tryParse(line) {
120
+ const match = line.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
121
+ if (match) {
122
+ clearTimeout(timeout);
123
+ resolve(match[0]);
124
+ }
125
+ }
126
+
127
+ child.stderr.on('data', (d) => tryParse(d.toString()));
128
+ child.stdout.on('data', (d) => tryParse(d.toString()));
129
+ child.on('error', () => { clearTimeout(timeout); resolve(null); });
130
+ });
131
+ }
@@ -0,0 +1,107 @@
1
+ // Prevent the system from sleeping while the server is running.
2
+ // On Windows this also covers lid-close (changes the action to "Do nothing").
3
+ // Everything is restored automatically on exit.
4
+
5
+ import { exec, spawn } from 'child_process';
6
+ import { promisify } from 'util';
7
+ const execAsync = promisify(exec);
8
+
9
+ // GUIDs for the Windows power buttons/lid subgroup
10
+ const LID_SUBGROUP = '4f971e89-eebd-4455-a8de-9e59040e7347';
11
+ const LID_ACTION = '5ca83367-6e45-459f-a27b-476b1d01c936';
12
+
13
+ let release = null;
14
+
15
+ export async function acquireWakeLock() {
16
+ try {
17
+ if (process.platform === 'win32') release = await windows();
18
+ else if (process.platform === 'darwin') release = await macos();
19
+ else release = await linux();
20
+
21
+ if (release) {
22
+ console.log(' Sleep + lid-close prevented while server is running.');
23
+ for (const sig of ['exit', 'SIGINT', 'SIGTERM', 'SIGHUP']) {
24
+ process.on(sig, () => { release?.(); release = null; });
25
+ }
26
+ }
27
+ } catch {
28
+ // Best-effort — never crash the server over this
29
+ }
30
+ }
31
+
32
+ // ── Windows ───────────────────────────────────────────────────────────────────
33
+ async function windows() {
34
+ // 1. Read the current AC lid-close action so we can restore it
35
+ let originalLidAc = '1'; // default: Sleep
36
+ let originalLidDc = '1';
37
+ try {
38
+ const { stdout } = await execAsync(
39
+ `powercfg /QUERY SCHEME_CURRENT ${LID_SUBGROUP} ${LID_ACTION}`
40
+ );
41
+ const acMatch = stdout.match(/Current AC Power Setting Index:\s+(0x[\da-fA-F]+)/);
42
+ const dcMatch = stdout.match(/Current DC Power Setting Index:\s+(0x[\da-fA-F]+)/);
43
+ if (acMatch) originalLidAc = parseInt(acMatch[1], 16).toString();
44
+ if (dcMatch) originalLidDc = parseInt(dcMatch[1], 16).toString();
45
+ } catch {}
46
+
47
+ // 2. Set lid close to "Do nothing" (0) on both AC and battery
48
+ try {
49
+ await execAsync(`powercfg /SETACVALUEINDEX SCHEME_CURRENT ${LID_SUBGROUP} ${LID_ACTION} 0`);
50
+ await execAsync(`powercfg /SETDCVALUEINDEX SCHEME_CURRENT ${LID_SUBGROUP} ${LID_ACTION} 0`);
51
+ await execAsync('powercfg /SETACTIVE SCHEME_CURRENT');
52
+ } catch {}
53
+
54
+ // 3. Spawn a PowerShell process that holds SetThreadExecutionState (blocks idle sleep)
55
+ const script = [
56
+ `$code = 'using System; using System.Runtime.InteropServices;`,
57
+ `public class WL { [DllImport("kernel32.dll")] public static extern uint STES(uint f); }'`,
58
+ `Add-Type -TypeDefinition $code -Language CSharp`,
59
+ `[WL]::STES(0x80000003)`, // ES_CONTINUOUS | ES_SYSTEM_REQUIRED
60
+ `while ($true) { Start-Sleep -Seconds 60 }`,
61
+ ].join('; ');
62
+
63
+ const child = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
64
+ stdio: 'ignore',
65
+ detached: false,
66
+ });
67
+ child.on('error', () => {});
68
+
69
+ return () => {
70
+ // Kill the sleep-prevention process
71
+ try { child.kill(); } catch {}
72
+
73
+ // Restore lid-close action
74
+ exec(`powercfg /SETACVALUEINDEX SCHEME_CURRENT ${LID_SUBGROUP} ${LID_ACTION} ${originalLidAc}`);
75
+ exec(`powercfg /SETDCVALUEINDEX SCHEME_CURRENT ${LID_SUBGROUP} ${LID_ACTION} ${originalLidDc}`);
76
+ exec('powercfg /SETACTIVE SCHEME_CURRENT');
77
+ };
78
+ }
79
+
80
+ // ── macOS ─────────────────────────────────────────────────────────────────────
81
+ // caffeinate -i = prevent idle sleep
82
+ // caffeinate -s = prevent system sleep (works with lid closed on AC)
83
+ async function macos() {
84
+ const child = spawn('caffeinate', ['-i', '-s'], {
85
+ stdio: 'ignore',
86
+ detached: false,
87
+ });
88
+ child.on('error', () => {});
89
+ return () => { try { child.kill(); } catch {} };
90
+ }
91
+
92
+ // ── Linux ─────────────────────────────────────────────────────────────────────
93
+ async function linux() {
94
+ try {
95
+ await execAsync('which systemd-inhibit');
96
+ const child = spawn(
97
+ 'systemd-inhibit',
98
+ ['--what=sleep:idle:handle-lid-switch', '--who=Conduit',
99
+ '--why=Terminal server running', 'sleep', 'infinity'],
100
+ { stdio: 'ignore', detached: false }
101
+ );
102
+ child.on('error', () => {});
103
+ return () => { try { child.kill(); } catch {} };
104
+ } catch {}
105
+
106
+ return null;
107
+ }