airloom 0.1.32 → 0.1.34
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/dist/index.js +348 -64
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -652,15 +652,28 @@ var AnthropicAdapter = class {
|
|
|
652
652
|
messages: chatMsgs
|
|
653
653
|
};
|
|
654
654
|
if (systemMsg) body.system = systemMsg;
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
655
|
+
const controller = new AbortController();
|
|
656
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
657
|
+
let response;
|
|
658
|
+
try {
|
|
659
|
+
response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: {
|
|
662
|
+
"Content-Type": "application/json",
|
|
663
|
+
"x-api-key": this.apiKey,
|
|
664
|
+
"anthropic-version": "2023-06-01"
|
|
665
|
+
},
|
|
666
|
+
body: JSON.stringify(body),
|
|
667
|
+
signal: controller.signal
|
|
668
|
+
});
|
|
669
|
+
} catch (err) {
|
|
670
|
+
clearTimeout(timeout);
|
|
671
|
+
const msg = err.name === "AbortError" ? "Request timed out" : err.message;
|
|
672
|
+
stream.write(`[Error: ${msg}]`);
|
|
673
|
+
stream.end();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
clearTimeout(timeout);
|
|
664
677
|
if (!response.ok) {
|
|
665
678
|
const error = await response.text();
|
|
666
679
|
stream.write(`[Error: ${response.status} ${error}]`);
|
|
@@ -714,14 +727,27 @@ var OpenAIAdapter = class {
|
|
|
714
727
|
this.baseUrl = config.baseUrl || "https://api.openai.com/v1";
|
|
715
728
|
}
|
|
716
729
|
async streamResponse(messages, stream) {
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
730
|
+
const controller = new AbortController();
|
|
731
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
732
|
+
let response;
|
|
733
|
+
try {
|
|
734
|
+
response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
735
|
+
method: "POST",
|
|
736
|
+
headers: {
|
|
737
|
+
"Content-Type": "application/json",
|
|
738
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
739
|
+
},
|
|
740
|
+
body: JSON.stringify({ model: this.model, stream: true, messages }),
|
|
741
|
+
signal: controller.signal
|
|
742
|
+
});
|
|
743
|
+
} catch (err) {
|
|
744
|
+
clearTimeout(timeout);
|
|
745
|
+
const msg = err.name === "AbortError" ? "Request timed out" : err.message;
|
|
746
|
+
stream.write(`[Error: ${msg}]`);
|
|
747
|
+
stream.end();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
clearTimeout(timeout);
|
|
725
751
|
if (!response.ok) {
|
|
726
752
|
const error = await response.text();
|
|
727
753
|
stream.write(`[Error: ${response.status} ${error}]`);
|
|
@@ -850,7 +876,7 @@ function resolveExecutable(command, envPath = process.env.PATH ?? "") {
|
|
|
850
876
|
}
|
|
851
877
|
for (const dir of envPath.split(delimiter)) {
|
|
852
878
|
if (!dir) continue;
|
|
853
|
-
const candidate = join(dir.replace(/^~(?=$|\/)/, process.env.HOME
|
|
879
|
+
const candidate = join(dir.replace(/^~(?=$|\/)/, process.env.HOME || process.env.USERPROFILE || "~"), command);
|
|
854
880
|
if (existsSync(candidate)) return candidate;
|
|
855
881
|
}
|
|
856
882
|
return null;
|
|
@@ -1123,7 +1149,8 @@ function resolveExecutable2(command, envPath = process.env.PATH ?? "") {
|
|
|
1123
1149
|
}
|
|
1124
1150
|
for (const dir of envPath.split(delimiter2)) {
|
|
1125
1151
|
if (!dir) continue;
|
|
1126
|
-
const
|
|
1152
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
1153
|
+
const candidate = join3(dir.replace(/^~(?=$|\/)/, home), command);
|
|
1127
1154
|
if (existsSync2(candidate)) return candidate;
|
|
1128
1155
|
}
|
|
1129
1156
|
return null;
|
|
@@ -1138,14 +1165,16 @@ function parseCommand(command) {
|
|
|
1138
1165
|
function getDefaultTerminalCommand(explicitCommand) {
|
|
1139
1166
|
const configured = explicitCommand?.trim() || process.env.AIRLOOM_TERMINAL_COMMAND?.trim();
|
|
1140
1167
|
if (configured) return parseCommand(configured);
|
|
1168
|
+
const shell = process.env.SHELL;
|
|
1169
|
+
if (shell) {
|
|
1170
|
+
const name = basename(shell);
|
|
1171
|
+
if (name === "bash" || name === "zsh" || name === "sh") return { file: shell, args: ["-il"] };
|
|
1172
|
+
return { file: shell, args: ["-i"] };
|
|
1173
|
+
}
|
|
1141
1174
|
if (process.platform === "win32") {
|
|
1142
|
-
|
|
1143
|
-
return { file, args: [] };
|
|
1175
|
+
return { file: process.env.COMSPEC || "powershell.exe", args: [] };
|
|
1144
1176
|
}
|
|
1145
|
-
|
|
1146
|
-
const name = basename(shell);
|
|
1147
|
-
if (name === "bash" || name === "zsh" || name === "sh") return { file: shell, args: ["-il"] };
|
|
1148
|
-
return { file: shell, args: ["-i"] };
|
|
1177
|
+
return { file: "/bin/bash", args: ["-il"] };
|
|
1149
1178
|
}
|
|
1150
1179
|
var AdaptiveOutputBatcher = class {
|
|
1151
1180
|
constructor(onFlush, fastInterval = 16, slowInterval = 80, interactiveWindow = 250, maxBytes = 4096) {
|
|
@@ -1220,8 +1249,8 @@ var TerminalSession = class {
|
|
|
1220
1249
|
const file = resolveExecutable2(command.file) ?? command.file;
|
|
1221
1250
|
const cwd = process.cwd();
|
|
1222
1251
|
log(`[host] PTY spawn: ${file} ${command.args.join(" ")} (${this.cols}x${this.rows}) node=${process.version}`);
|
|
1223
|
-
const
|
|
1224
|
-
const spawnOpts = { name: "xterm-256color", cols: this.cols, rows: this.rows, cwd, env
|
|
1252
|
+
const env = { ...process.env, TERM: "xterm-256color" };
|
|
1253
|
+
const spawnOpts = { name: "xterm-256color", cols: this.cols, rows: this.rows, cwd, env };
|
|
1225
1254
|
let nodePty;
|
|
1226
1255
|
try {
|
|
1227
1256
|
nodePty = requireNodePty();
|
|
@@ -2176,8 +2205,8 @@ function parseRelayUrl(value) {
|
|
|
2176
2205
|
}
|
|
2177
2206
|
return parsed.toString();
|
|
2178
2207
|
}
|
|
2179
|
-
function parseHostBind(value, isDev) {
|
|
2180
|
-
const bind = value?.trim() || (isDev ? "0.0.0.0" : DEFAULT_HOST_BIND);
|
|
2208
|
+
function parseHostBind(value, isDev, isSSH) {
|
|
2209
|
+
const bind = value?.trim() || (isDev || isSSH ? "0.0.0.0" : DEFAULT_HOST_BIND);
|
|
2181
2210
|
if (bind === "localhost" || isIP(bind) !== 0) return bind;
|
|
2182
2211
|
if (/^[A-Za-z0-9.-]+$/.test(bind)) return bind;
|
|
2183
2212
|
throw new Error("HOST_BIND must be localhost, an IP address, or a hostname");
|
|
@@ -2186,11 +2215,12 @@ function isLoopbackBind(bind) {
|
|
|
2186
2215
|
return bind === "127.0.0.1" || bind === "::1" || bind === "localhost";
|
|
2187
2216
|
}
|
|
2188
2217
|
function parseHostEnv(cliPort, isDev = false) {
|
|
2218
|
+
const isSSH = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
|
|
2189
2219
|
const relayUrl = parseRelayUrl(process.env.RELAY_URL);
|
|
2190
2220
|
const ablyApiKey = process.env.ABLY_API_KEY ?? (relayUrl ? void 0 : DEFAULT_ABLY_KEY);
|
|
2191
2221
|
const ablyTokenTtlMs = parseInteger("ABLY_TOKEN_TTL", process.env.ABLY_TOKEN_TTL, DEFAULT_ABLY_TOKEN_TTL_MS, 6e4, 31 * 24 * 60 * 60 * 1e3);
|
|
2192
2222
|
const hostPort = cliPort ?? parseInteger("HOST_PORT", process.env.HOST_PORT, DEFAULT_HOST_PORT, 0, 65535);
|
|
2193
|
-
const hostBind = parseHostBind(process.env.HOST_BIND, isDev);
|
|
2223
|
+
const hostBind = parseHostBind(process.env.HOST_BIND, isDev, isSSH);
|
|
2194
2224
|
const viewerUrl = parseViewerUrl(process.env.VIEWER_URL);
|
|
2195
2225
|
return {
|
|
2196
2226
|
viewerUrl,
|
|
@@ -2200,7 +2230,8 @@ function parseHostEnv(cliPort, isDev = false) {
|
|
|
2200
2230
|
hostPort,
|
|
2201
2231
|
hostBind,
|
|
2202
2232
|
useAbly: !!ablyApiKey,
|
|
2203
|
-
isDefaultAblyKey: !!ablyApiKey && ablyApiKey === DEFAULT_ABLY_KEY
|
|
2233
|
+
isDefaultAblyKey: !!ablyApiKey && ablyApiKey === DEFAULT_ABLY_KEY,
|
|
2234
|
+
isSSH
|
|
2204
2235
|
};
|
|
2205
2236
|
}
|
|
2206
2237
|
|
|
@@ -2236,13 +2267,213 @@ function loadOrCreateAblySessionToken() {
|
|
|
2236
2267
|
return ablySessionToken;
|
|
2237
2268
|
}
|
|
2238
2269
|
|
|
2270
|
+
// src/daemon.ts
|
|
2271
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, readdirSync, openSync, closeSync } from "node:fs";
|
|
2272
|
+
import { homedir as homedir3 } from "node:os";
|
|
2273
|
+
import { join as join5 } from "node:path";
|
|
2274
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2275
|
+
var SESSIONS_DIR = join5(homedir3(), ".config", "airloom", "sessions");
|
|
2276
|
+
function ensureDir() {
|
|
2277
|
+
mkdirSync3(SESSIONS_DIR, { recursive: true });
|
|
2278
|
+
}
|
|
2279
|
+
function sessionPath(name) {
|
|
2280
|
+
return join5(SESSIONS_DIR, `${name}.json`);
|
|
2281
|
+
}
|
|
2282
|
+
function logFilePath(name) {
|
|
2283
|
+
return join5(SESSIONS_DIR, `${name}.log`);
|
|
2284
|
+
}
|
|
2285
|
+
function validateName(name) {
|
|
2286
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2287
|
+
throw new Error(`Invalid session name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
function readSession(name) {
|
|
2291
|
+
try {
|
|
2292
|
+
const raw = readFileSync3(sessionPath(name), "utf-8");
|
|
2293
|
+
const data = JSON.parse(raw);
|
|
2294
|
+
if (!data || typeof data.pid !== "number") return null;
|
|
2295
|
+
return data;
|
|
2296
|
+
} catch {
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
function writeSession(name, info) {
|
|
2301
|
+
ensureDir();
|
|
2302
|
+
writeFileSync3(sessionPath(name), JSON.stringify(info, null, 2) + "\n", { mode: 384 });
|
|
2303
|
+
}
|
|
2304
|
+
function removeSession(name) {
|
|
2305
|
+
try {
|
|
2306
|
+
unlinkSync(sessionPath(name));
|
|
2307
|
+
} catch {
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
function isAlive(pid) {
|
|
2311
|
+
try {
|
|
2312
|
+
process.kill(pid, 0);
|
|
2313
|
+
return true;
|
|
2314
|
+
} catch {
|
|
2315
|
+
return false;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
function listAllSessions() {
|
|
2319
|
+
ensureDir();
|
|
2320
|
+
let files;
|
|
2321
|
+
try {
|
|
2322
|
+
files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
|
|
2323
|
+
} catch {
|
|
2324
|
+
return [];
|
|
2325
|
+
}
|
|
2326
|
+
const results = [];
|
|
2327
|
+
for (const file of files) {
|
|
2328
|
+
const name = file.replace(/\.json$/, "");
|
|
2329
|
+
const info = readSession(name);
|
|
2330
|
+
if (!info) continue;
|
|
2331
|
+
if (!isAlive(info.pid)) {
|
|
2332
|
+
removeSession(name);
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
results.push({ name, info });
|
|
2336
|
+
}
|
|
2337
|
+
return results;
|
|
2338
|
+
}
|
|
2339
|
+
function formatAge(ms) {
|
|
2340
|
+
const sec = Math.floor(ms / 1e3);
|
|
2341
|
+
if (sec < 60) return `${sec}s ago`;
|
|
2342
|
+
const min = Math.floor(sec / 60);
|
|
2343
|
+
if (min < 60) return `${min}m ago`;
|
|
2344
|
+
const hr = Math.floor(min / 60);
|
|
2345
|
+
if (hr < 24) return `${hr}h ${min % 60}m ago`;
|
|
2346
|
+
return `${Math.floor(hr / 24)}d ago`;
|
|
2347
|
+
}
|
|
2348
|
+
async function handleStart(name, hostArgs) {
|
|
2349
|
+
validateName(name);
|
|
2350
|
+
const existing = readSession(name);
|
|
2351
|
+
if (existing && isAlive(existing.pid)) {
|
|
2352
|
+
console.error(`Session "${name}" is already running (PID ${existing.pid}, port ${existing.port})`);
|
|
2353
|
+
console.error(`Host UI: ${existing.controlUrl}`);
|
|
2354
|
+
process.exit(1);
|
|
2355
|
+
}
|
|
2356
|
+
if (existing) removeSession(name);
|
|
2357
|
+
ensureDir();
|
|
2358
|
+
const logFile = logFilePath(name);
|
|
2359
|
+
const logFd = openSync(logFile, "w");
|
|
2360
|
+
const child = spawn2(
|
|
2361
|
+
process.execPath,
|
|
2362
|
+
[...process.execArgv, process.argv[1], ...hostArgs, "--_daemon"],
|
|
2363
|
+
{
|
|
2364
|
+
detached: true,
|
|
2365
|
+
stdio: ["ignore", logFd, logFd, "ipc"],
|
|
2366
|
+
cwd: process.cwd(),
|
|
2367
|
+
env: process.env
|
|
2368
|
+
}
|
|
2369
|
+
);
|
|
2370
|
+
try {
|
|
2371
|
+
const info = await new Promise((resolve4, reject) => {
|
|
2372
|
+
const timer = setTimeout(() => {
|
|
2373
|
+
reject(new Error(`Timed out waiting for daemon to start (30s). Check log: ${logFile}`));
|
|
2374
|
+
}, 3e4);
|
|
2375
|
+
child.on("message", (msg) => {
|
|
2376
|
+
const m = msg;
|
|
2377
|
+
if (m.type === "ready") {
|
|
2378
|
+
clearTimeout(timer);
|
|
2379
|
+
resolve4({
|
|
2380
|
+
pid: child.pid,
|
|
2381
|
+
port: m.port,
|
|
2382
|
+
controlUrl: m.controlUrl,
|
|
2383
|
+
viewerUrl: m.viewerUrl,
|
|
2384
|
+
pairingCode: m.pairingCode,
|
|
2385
|
+
cwd: process.cwd(),
|
|
2386
|
+
startedAt: Date.now(),
|
|
2387
|
+
logFile
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
child.on("error", (err) => {
|
|
2392
|
+
clearTimeout(timer);
|
|
2393
|
+
reject(err);
|
|
2394
|
+
});
|
|
2395
|
+
child.on("exit", (code) => {
|
|
2396
|
+
clearTimeout(timer);
|
|
2397
|
+
reject(new Error(`Daemon exited with code ${code}. Check log: ${logFile}`));
|
|
2398
|
+
});
|
|
2399
|
+
});
|
|
2400
|
+
writeSession(name, info);
|
|
2401
|
+
child.disconnect();
|
|
2402
|
+
child.unref();
|
|
2403
|
+
closeSync(logFd);
|
|
2404
|
+
console.log(`
|
|
2405
|
+
Airloom session "${name}" started (PID ${info.pid})
|
|
2406
|
+
`);
|
|
2407
|
+
console.log(`Pairing Code: ${info.pairingCode}`);
|
|
2408
|
+
console.log(`Viewer URL: ${info.viewerUrl}`);
|
|
2409
|
+
console.log(`Host UI: ${info.controlUrl}`);
|
|
2410
|
+
console.log(`Log: ${info.logFile}
|
|
2411
|
+
`);
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
closeSync(logFd);
|
|
2414
|
+
try {
|
|
2415
|
+
child.kill();
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
throw err;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function handleStop(nameOrNull, all) {
|
|
2422
|
+
if (all) {
|
|
2423
|
+
const sessions = listAllSessions();
|
|
2424
|
+
if (sessions.length === 0) {
|
|
2425
|
+
console.log("No running sessions.");
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
for (const { name, info: info2 } of sessions) {
|
|
2429
|
+
try {
|
|
2430
|
+
process.kill(info2.pid, "SIGTERM");
|
|
2431
|
+
} catch {
|
|
2432
|
+
}
|
|
2433
|
+
removeSession(name);
|
|
2434
|
+
console.log(`Stopped "${name}" (PID ${info2.pid})`);
|
|
2435
|
+
}
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const target = nameOrNull || "default";
|
|
2439
|
+
const info = readSession(target);
|
|
2440
|
+
if (!info) {
|
|
2441
|
+
console.error(`No session named "${target}" found.`);
|
|
2442
|
+
process.exit(1);
|
|
2443
|
+
}
|
|
2444
|
+
if (!isAlive(info.pid)) {
|
|
2445
|
+
removeSession(target);
|
|
2446
|
+
console.log(`Session "${target}" was not running (cleaned up stale entry).`);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
try {
|
|
2450
|
+
process.kill(info.pid, "SIGTERM");
|
|
2451
|
+
} catch {
|
|
2452
|
+
}
|
|
2453
|
+
removeSession(target);
|
|
2454
|
+
console.log(`Stopped "${target}" (PID ${info.pid})`);
|
|
2455
|
+
}
|
|
2456
|
+
function handleList() {
|
|
2457
|
+
const sessions = listAllSessions();
|
|
2458
|
+
if (sessions.length === 0) {
|
|
2459
|
+
console.log("No running sessions.");
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
console.log("NAME PORT PID STARTED CWD");
|
|
2463
|
+
for (const { name, info } of sessions) {
|
|
2464
|
+
const age = formatAge(Date.now() - info.startedAt);
|
|
2465
|
+
console.log(
|
|
2466
|
+
name.padEnd(16) + String(info.port).padEnd(7) + String(info.pid).padEnd(9) + age.padEnd(17) + info.cwd
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2239
2471
|
// src/index.ts
|
|
2240
2472
|
var QRCode = null;
|
|
2241
2473
|
async function getQRCode() {
|
|
2242
2474
|
if (!QRCode) QRCode = await import("qrcode");
|
|
2243
2475
|
return QRCode;
|
|
2244
2476
|
}
|
|
2245
|
-
log("[host] Module loaded");
|
|
2246
2477
|
function parseArgs(argv) {
|
|
2247
2478
|
const args = {};
|
|
2248
2479
|
const rest = argv.slice(2);
|
|
@@ -2272,7 +2503,15 @@ function printHelp() {
|
|
|
2272
2503
|
Airloom \u2014 Run AI on your computer, control it from your phone.
|
|
2273
2504
|
|
|
2274
2505
|
Usage:
|
|
2275
|
-
airloom [options]
|
|
2506
|
+
airloom [options] Start in foreground (default)
|
|
2507
|
+
airloom start [options] Start as a background daemon
|
|
2508
|
+
airloom stop [name] Stop a background session
|
|
2509
|
+
airloom stop --all Stop all background sessions
|
|
2510
|
+
airloom list List running background sessions
|
|
2511
|
+
|
|
2512
|
+
Background options:
|
|
2513
|
+
--name <name> Session name (default: "default").
|
|
2514
|
+
Allows multiple independent sessions.
|
|
2276
2515
|
|
|
2277
2516
|
Options:
|
|
2278
2517
|
--cli <command> CLI command to use as the AI adapter.
|
|
@@ -2297,22 +2536,6 @@ Environment variables:
|
|
|
2297
2536
|
HOST_BIND Host bind address (default: 127.0.0.1).
|
|
2298
2537
|
`.trimStart());
|
|
2299
2538
|
}
|
|
2300
|
-
var cliArgs = parseArgs(process.argv);
|
|
2301
|
-
if (cliArgs.help) {
|
|
2302
|
-
printHelp();
|
|
2303
|
-
process.exit(0);
|
|
2304
|
-
}
|
|
2305
|
-
var IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
|
|
2306
|
-
var env = parseHostEnv(cliArgs.port, IS_DEV);
|
|
2307
|
-
var VIEWER_URL = env.viewerUrl;
|
|
2308
|
-
var RELAY_URL = env.relayUrl;
|
|
2309
|
-
var ABLY_API_KEY = env.ablyApiKey;
|
|
2310
|
-
var ABLY_TOKEN_TTL = env.ablyTokenTtlMs;
|
|
2311
|
-
var HOST_PORT = env.hostPort;
|
|
2312
|
-
var HOST_BIND = env.hostBind;
|
|
2313
|
-
var useAbly = env.useAbly;
|
|
2314
|
-
var isDefaultKey = env.isDefaultAblyKey;
|
|
2315
|
-
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2316
2539
|
function getLanIP() {
|
|
2317
2540
|
for (const ifaces of Object.values(networkInterfaces())) {
|
|
2318
2541
|
for (const iface of ifaces ?? []) {
|
|
@@ -2321,14 +2544,31 @@ function getLanIP() {
|
|
|
2321
2544
|
}
|
|
2322
2545
|
return void 0;
|
|
2323
2546
|
}
|
|
2324
|
-
function resolveViewerDir() {
|
|
2325
|
-
const prod = resolve3(
|
|
2547
|
+
function resolveViewerDir(base) {
|
|
2548
|
+
const prod = resolve3(base, "viewer");
|
|
2326
2549
|
if (existsSync4(prod)) return prod;
|
|
2327
|
-
const dev = resolve3(
|
|
2550
|
+
const dev = resolve3(base, "../../viewer/dist");
|
|
2328
2551
|
if (existsSync4(dev)) return dev;
|
|
2329
2552
|
return void 0;
|
|
2330
2553
|
}
|
|
2331
2554
|
async function main() {
|
|
2555
|
+
const cliArgs = parseArgs(process.argv);
|
|
2556
|
+
if (cliArgs.help) {
|
|
2557
|
+
printHelp();
|
|
2558
|
+
process.exit(0);
|
|
2559
|
+
}
|
|
2560
|
+
const IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
|
|
2561
|
+
const env = parseHostEnv(cliArgs.port, IS_DEV);
|
|
2562
|
+
const VIEWER_URL = env.viewerUrl;
|
|
2563
|
+
const RELAY_URL = env.relayUrl;
|
|
2564
|
+
const ABLY_API_KEY = env.ablyApiKey;
|
|
2565
|
+
const ABLY_TOKEN_TTL = env.ablyTokenTtlMs;
|
|
2566
|
+
const HOST_PORT = env.hostPort;
|
|
2567
|
+
const HOST_BIND = env.hostBind;
|
|
2568
|
+
const useAbly = env.useAbly;
|
|
2569
|
+
const isDefaultKey = env.isDefaultAblyKey;
|
|
2570
|
+
const isDaemonChild = process.argv.includes("--_daemon");
|
|
2571
|
+
const __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2332
2572
|
console.log("Airloom - Host");
|
|
2333
2573
|
console.log("==============\n");
|
|
2334
2574
|
if (useAbly) {
|
|
@@ -2480,7 +2720,7 @@ async function main() {
|
|
|
2480
2720
|
}
|
|
2481
2721
|
}
|
|
2482
2722
|
}
|
|
2483
|
-
const viewerDir = resolveViewerDir();
|
|
2723
|
+
const viewerDir = resolveViewerDir(__dirname);
|
|
2484
2724
|
if (viewerDir) {
|
|
2485
2725
|
log(`[host] Viewer files: ${viewerDir}`);
|
|
2486
2726
|
} else {
|
|
@@ -2517,16 +2757,23 @@ async function main() {
|
|
|
2517
2757
|
if (lanViewerUrl) console.log(`LAN Viewer: ${lanViewerUrl}`);
|
|
2518
2758
|
}
|
|
2519
2759
|
if (!useAbly) console.log(`Relay: ${RELAY_URL}`);
|
|
2520
|
-
const
|
|
2521
|
-
const controlUrl = encodeControlUrl(
|
|
2760
|
+
const controlBase = isLoopbackBind(HOST_BIND) ? `http://localhost:${port}` : lanBaseUrl;
|
|
2761
|
+
const controlUrl = encodeControlUrl(controlBase, controlToken);
|
|
2522
2762
|
console.log(`Host UI: ${controlUrl}`);
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2763
|
+
if (isDaemonChild && typeof process.send === "function") {
|
|
2764
|
+
process.send({ type: "ready", port, controlUrl, viewerUrl: qrTarget, pairingCode: displayCode });
|
|
2765
|
+
}
|
|
2766
|
+
if (isDaemonChild) {
|
|
2767
|
+
} else if (env.isSSH) {
|
|
2768
|
+
if (isLoopbackBind(HOST_BIND)) {
|
|
2769
|
+
console.log("\n (SSH session detected but server is bound to localhost \u2014 set HOST_BIND=0.0.0.0 to allow remote access)");
|
|
2770
|
+
} else {
|
|
2771
|
+
console.log("\n (SSH session \u2014 open the Host UI URL above in a browser on your local machine)");
|
|
2772
|
+
}
|
|
2526
2773
|
} else {
|
|
2527
|
-
import("node:child_process").then(({
|
|
2774
|
+
import("node:child_process").then(({ execFile }) => {
|
|
2528
2775
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2529
|
-
|
|
2776
|
+
execFile(cmd, [controlUrl]);
|
|
2530
2777
|
}).catch(() => {
|
|
2531
2778
|
});
|
|
2532
2779
|
}
|
|
@@ -2593,7 +2840,44 @@ async function main() {
|
|
|
2593
2840
|
process.on("SIGINT", shutdown);
|
|
2594
2841
|
process.on("SIGTERM", shutdown);
|
|
2595
2842
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2843
|
+
var _cmd = process.argv[2];
|
|
2844
|
+
if (_cmd === "start" || _cmd === "stop" || _cmd === "list") {
|
|
2845
|
+
(async () => {
|
|
2846
|
+
const _args = process.argv.slice(3);
|
|
2847
|
+
if (_cmd === "list") {
|
|
2848
|
+
handleList();
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
if (_cmd === "stop") {
|
|
2852
|
+
let stopName = null;
|
|
2853
|
+
let stopAll = false;
|
|
2854
|
+
for (const a of _args) {
|
|
2855
|
+
if (a === "--all") stopAll = true;
|
|
2856
|
+
else if (!a.startsWith("-")) stopName = a;
|
|
2857
|
+
}
|
|
2858
|
+
handleStop(stopName, stopAll);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
let startName = "default";
|
|
2862
|
+
const hostArgs = [];
|
|
2863
|
+
for (let i = 0; i < _args.length; i++) {
|
|
2864
|
+
const a = _args[i];
|
|
2865
|
+
if (a === "--name" && i + 1 < _args.length) {
|
|
2866
|
+
startName = _args[++i];
|
|
2867
|
+
} else if (a.startsWith("--name=")) {
|
|
2868
|
+
startName = a.slice(7);
|
|
2869
|
+
} else {
|
|
2870
|
+
hostArgs.push(a);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
await handleStart(startName, hostArgs);
|
|
2874
|
+
})().catch((err) => {
|
|
2875
|
+
console.error(err.message);
|
|
2876
|
+
process.exit(1);
|
|
2877
|
+
});
|
|
2878
|
+
} else {
|
|
2879
|
+
main().catch((err) => {
|
|
2880
|
+
logError("Fatal error:", err);
|
|
2881
|
+
process.exit(1);
|
|
2882
|
+
});
|
|
2883
|
+
}
|