airloom 0.1.13 → 0.1.16
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 +261 -351
- package/dist/viewer/assets/{browser-BRKgrEB1.js → browser-uOqU-WQO.js} +1 -1
- package/dist/viewer/assets/{index-CCYyPnVc.js → index-BkOD4g6Y.js} +17 -17
- package/dist/viewer/assets/{index-D9J9LZLl.css → index-C27Ct95O.css} +1 -1
- package/dist/viewer/assets/{index-DKFwSRim.js → index-hJGxFf4H.js} +1 -1
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/sw.js +30 -86
- package/package.json +9 -7
package/dist/index.js
CHANGED
|
@@ -600,16 +600,15 @@ var AblyAdapter = class {
|
|
|
600
600
|
|
|
601
601
|
// src/index.ts
|
|
602
602
|
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
603
|
-
import { networkInterfaces } from "os";
|
|
604
|
-
import { fileURLToPath } from "url";
|
|
605
|
-
import { dirname, resolve as resolve3 } from "path";
|
|
606
|
-
import { existsSync as existsSync4 } from "fs";
|
|
607
|
-
import QRCode from "qrcode";
|
|
603
|
+
import { networkInterfaces } from "node:os";
|
|
604
|
+
import { fileURLToPath } from "node:url";
|
|
605
|
+
import { dirname, resolve as resolve3 } from "node:path";
|
|
606
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
608
607
|
|
|
609
608
|
// src/server.ts
|
|
610
609
|
import express from "express";
|
|
611
|
-
import { createServer } from "http";
|
|
612
|
-
import { existsSync as existsSync3 } from "fs";
|
|
610
|
+
import { createServer } from "node:http";
|
|
611
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
613
612
|
import { WebSocketServer, WebSocket } from "ws";
|
|
614
613
|
|
|
615
614
|
// src/adapters/anthropic.ts
|
|
@@ -742,9 +741,9 @@ var OpenAIAdapter = class {
|
|
|
742
741
|
};
|
|
743
742
|
|
|
744
743
|
// src/adapters/cli.ts
|
|
745
|
-
import { spawn } from "child_process";
|
|
746
|
-
import { existsSync } from "fs";
|
|
747
|
-
import { delimiter, isAbsolute, join, resolve } from "path";
|
|
744
|
+
import { spawn } from "node:child_process";
|
|
745
|
+
import { existsSync } from "node:fs";
|
|
746
|
+
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
748
747
|
var CLI_PRESETS = [
|
|
749
748
|
{
|
|
750
749
|
id: "devin",
|
|
@@ -998,9 +997,9 @@ var CLIAdapter = class {
|
|
|
998
997
|
};
|
|
999
998
|
|
|
1000
999
|
// src/config.ts
|
|
1001
|
-
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
1002
|
-
import { homedir } from "os";
|
|
1003
|
-
import { join as join2 } from "path";
|
|
1000
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
1001
|
+
import { homedir } from "node:os";
|
|
1002
|
+
import { join as join2 } from "node:path";
|
|
1004
1003
|
var CONFIG_DIR = join2(homedir(), ".config", "airloom");
|
|
1005
1004
|
var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
1006
1005
|
function loadConfig() {
|
|
@@ -1026,8 +1025,8 @@ function getConfigPath() {
|
|
|
1026
1025
|
}
|
|
1027
1026
|
|
|
1028
1027
|
// src/terminal.ts
|
|
1029
|
-
import { basename, delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
|
|
1030
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1028
|
+
import { basename, delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
|
|
1029
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1031
1030
|
import { spawn as spawn2 } from "node-pty";
|
|
1032
1031
|
function resolveExecutable2(command, envPath = process.env.PATH ?? "") {
|
|
1033
1032
|
if (!command) return null;
|
|
@@ -1113,9 +1112,10 @@ function getTerminalLaunchDisplay(explicitCommand) {
|
|
|
1113
1112
|
return [command.file, ...command.args].join(" ");
|
|
1114
1113
|
}
|
|
1115
1114
|
var TerminalSession = class {
|
|
1116
|
-
constructor(channel, getLaunchCommand) {
|
|
1115
|
+
constructor(channel, getLaunchCommand, broadcastFn) {
|
|
1117
1116
|
this.channel = channel;
|
|
1118
1117
|
this.getLaunchCommand = getLaunchCommand;
|
|
1118
|
+
this.broadcastFn = broadcastFn;
|
|
1119
1119
|
}
|
|
1120
1120
|
pty = null;
|
|
1121
1121
|
stream = null;
|
|
@@ -1156,13 +1156,23 @@ var TerminalSession = class {
|
|
|
1156
1156
|
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1157
1157
|
if (this.pty) {
|
|
1158
1158
|
this.pty.resize(this.cols, this.rows);
|
|
1159
|
+
if (!this.stream || this.stream.ended) {
|
|
1160
|
+
const meta2 = { kind: "terminal", cols: this.cols, rows: this.rows };
|
|
1161
|
+
this.stream = this.channel.createStream(meta2);
|
|
1162
|
+
this.batcher = new AdaptiveOutputBatcher((data) => {
|
|
1163
|
+
this.stream?.write(data);
|
|
1164
|
+
});
|
|
1165
|
+
this.pty.write("\f");
|
|
1166
|
+
}
|
|
1159
1167
|
return;
|
|
1160
1168
|
}
|
|
1161
1169
|
const command = getDefaultTerminalCommand(this.getLaunchCommand?.());
|
|
1162
1170
|
const file = resolveExecutable2(command.file) ?? command.file;
|
|
1163
1171
|
const meta = { kind: "terminal", cols: this.cols, rows: this.rows };
|
|
1164
1172
|
this.stream = this.channel.createStream(meta);
|
|
1165
|
-
this.batcher = new AdaptiveOutputBatcher((data) =>
|
|
1173
|
+
this.batcher = new AdaptiveOutputBatcher((data) => {
|
|
1174
|
+
this.stream?.write(data);
|
|
1175
|
+
});
|
|
1166
1176
|
const env = { ...process.env, TERM: "xterm-256color" };
|
|
1167
1177
|
this.pty = spawn2(file, command.args, {
|
|
1168
1178
|
name: "xterm-256color",
|
|
@@ -1171,7 +1181,11 @@ var TerminalSession = class {
|
|
|
1171
1181
|
cwd: process.cwd(),
|
|
1172
1182
|
env
|
|
1173
1183
|
});
|
|
1174
|
-
this.pty.onData((data) =>
|
|
1184
|
+
this.pty.onData((data) => {
|
|
1185
|
+
process.stdout.write(data);
|
|
1186
|
+
this.batcher?.write(data);
|
|
1187
|
+
this.broadcastFn?.({ type: "terminal_output", data });
|
|
1188
|
+
});
|
|
1175
1189
|
this.pty.onExit(({ exitCode, signal }) => {
|
|
1176
1190
|
this.batcher?.flush();
|
|
1177
1191
|
this.stream?.end();
|
|
@@ -1186,6 +1200,11 @@ var TerminalSession = class {
|
|
|
1186
1200
|
this.batcher?.noteInput();
|
|
1187
1201
|
this.pty.write(message.data);
|
|
1188
1202
|
}
|
|
1203
|
+
writeRawInput(data) {
|
|
1204
|
+
if (!this.pty) return;
|
|
1205
|
+
this.batcher?.noteInput();
|
|
1206
|
+
this.pty.write(data);
|
|
1207
|
+
}
|
|
1189
1208
|
resize(message) {
|
|
1190
1209
|
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1191
1210
|
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
@@ -1310,16 +1329,30 @@ function createHostServer(opts) {
|
|
|
1310
1329
|
}
|
|
1311
1330
|
res.json({ ok: true });
|
|
1312
1331
|
});
|
|
1332
|
+
app.get("/api/pair", (req, res) => {
|
|
1333
|
+
const { session } = req.query;
|
|
1334
|
+
if (!session || session !== opts.state.sessionToken) {
|
|
1335
|
+
res.status(401).json({ error: "Invalid session" });
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
res.json({
|
|
1339
|
+
token: opts.state.ablyToken,
|
|
1340
|
+
transport: opts.state.transport ?? "ws",
|
|
1341
|
+
relay: opts.state.relayUrl
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1313
1344
|
wss.on("connection", (ws) => {
|
|
1314
1345
|
uiClients.add(ws);
|
|
1315
1346
|
ws.on("close", () => uiClients.delete(ws));
|
|
1316
1347
|
ws.on("message", (data) => {
|
|
1317
1348
|
try {
|
|
1318
1349
|
const message = JSON.parse(data.toString());
|
|
1319
|
-
if (message.type && message.
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1350
|
+
if (message.type === "terminal_input" && typeof message.data === "string") {
|
|
1351
|
+
const filtered = message.data.replace(/\x1b\]\d+;[^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
|
|
1352
|
+
if (!filtered) return;
|
|
1353
|
+
opts.state.terminal?.writeRawInput(filtered);
|
|
1354
|
+
} else if (message.type === "terminal_resize") {
|
|
1355
|
+
opts.state.terminal?.handleMessage(message);
|
|
1323
1356
|
}
|
|
1324
1357
|
} catch (err) {
|
|
1325
1358
|
console.error("[host] Invalid WebSocket message:", err);
|
|
@@ -1389,6 +1422,7 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1389
1422
|
<meta charset="UTF-8">
|
|
1390
1423
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1391
1424
|
<title>Airloom - Host</title>
|
|
1425
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.css">
|
|
1392
1426
|
<style>
|
|
1393
1427
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
1394
1428
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0a;color:#e0e0e0;min-height:100vh}
|
|
@@ -1411,39 +1445,14 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1411
1445
|
button{background:#7c8aff;color:#fff;border:none;cursor:pointer;font-weight:600}
|
|
1412
1446
|
button:hover{background:#6b79ee}
|
|
1413
1447
|
.messages{max-height:400px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
|
1414
|
-
.msg{padding:10px 14px;border-radius:10px;max-width:85%;word-break:break-word;font-size:.9rem;line-height:1.5}
|
|
1415
|
-
.msg.user{background:#2a3a6a;align-self:flex-end
|
|
1448
|
+
.msg{padding:10px 14px;border-radius:10px;max-width:85%;word-break:break-word;font-size:.9rem;line-height:1.5;white-space:pre-wrap}
|
|
1449
|
+
.msg.user{background:#2a3a6a;align-self:flex-end}
|
|
1416
1450
|
.msg.assistant{background:#1e1e1e;border:1px solid #2a2a2a;align-self:flex-start}
|
|
1417
|
-
.msg.assistant p{margin:0 0 .5em}.msg.assistant p:last-child{margin-bottom:0}
|
|
1418
|
-
.msg.assistant h1,.msg.assistant h2,.msg.assistant h3,.msg.assistant h4,.msg.assistant h5,.msg.assistant h6{font-size:.95rem;font-weight:600;margin:.6em 0 .3em;color:#e0e0e0}
|
|
1419
|
-
.msg.assistant h1{font-size:1.05rem}.msg.assistant h2{font-size:1rem}
|
|
1420
|
-
.msg.assistant ul,.msg.assistant ol{margin:.3em 0;padding-left:1.4em}.msg.assistant li{margin:.15em 0}
|
|
1421
|
-
.msg.assistant a{color:#7c8aff;text-decoration:underline}
|
|
1422
|
-
.msg.assistant code{font-family:'SF Mono',Menlo,Consolas,monospace;font-size:.82rem;background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px}
|
|
1423
|
-
.msg.assistant pre{margin:.5em 0;padding:10px 12px;border-radius:8px;background:#111;overflow-x:auto}
|
|
1424
|
-
.msg.assistant pre code{background:none;padding:0;font-size:.8rem;line-height:1.5;white-space:pre;display:block}
|
|
1425
|
-
.msg.assistant blockquote{margin:.4em 0;padding:4px 12px;border-left:3px solid #2a2a2a;color:#888}
|
|
1426
|
-
.msg.assistant table{border-collapse:collapse;margin:.4em 0;font-size:.82rem}
|
|
1427
|
-
.msg.assistant th,.msg.assistant td{border:1px solid #2a2a2a;padding:4px 8px}
|
|
1428
|
-
.msg.assistant th{background:rgba(255,255,255,.05)}
|
|
1429
|
-
.msg.assistant hr{border:none;border-top:1px solid #2a2a2a;margin:.5em 0}
|
|
1430
|
-
.msg.typing{display:flex;align-items:center;gap:5px;padding:14px 18px}
|
|
1431
|
-
.msg.typing .dot{width:7px;height:7px;border-radius:50%;background:#888;animation:dot-pulse 1.4s ease-in-out infinite}
|
|
1432
|
-
.msg.typing .dot:nth-child(2){animation-delay:.2s}
|
|
1433
|
-
.msg.typing .dot:nth-child(3){animation-delay:.4s}
|
|
1434
|
-
@keyframes dot-pulse{0%,80%,100%{opacity:.25;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}
|
|
1435
1451
|
.input-area{display:flex;gap:8px}
|
|
1436
1452
|
.input-area textarea{flex:1;resize:none;min-height:44px;max-height:120px;padding:10px 14px;border-radius:8px;border:1px solid #333;background:#111;color:#e0e0e0;font-family:inherit;font-size:.9rem}
|
|
1437
|
-
|
|
1438
|
-
/* Terminal mode styles */
|
|
1439
|
-
.terminal-mode .chat-only { display: none !important; }
|
|
1440
|
-
.terminal-mode .terminal-only { display: block; }
|
|
1441
|
-
.chat-mode .terminal-only { display: none !important; }
|
|
1442
|
-
.chat-mode .chat-only { display: block; }
|
|
1443
|
-
|
|
1444
|
-
.terminal-container{background:#05070c;border:1px solid #2a2a2a;border-radius:8px;height:400px;overflow:hidden;margin-bottom:12px}
|
|
1453
|
+
.terminal-container{background:#05070c;border:1px solid #2a2a2a;border-radius:8px;height:420px;overflow:hidden;margin-bottom:12px}
|
|
1445
1454
|
#terminal{width:100%;height:100%;padding:8px}
|
|
1446
|
-
.
|
|
1455
|
+
.toolbar{display:flex;gap:8px;margin-bottom:12px}
|
|
1447
1456
|
.tool-btn{padding:6px 12px;font-size:.85rem;background:#333;border:none;border-radius:6px;color:#e0e0e0;cursor:pointer}
|
|
1448
1457
|
.tool-btn:hover{background:#444}
|
|
1449
1458
|
</style>
|
|
@@ -1458,360 +1467,251 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1458
1467
|
<div class="status"><div class="dot wait" id="dot"></div><span id="statusText">Initializing...</span></div>
|
|
1459
1468
|
<p style="color:#888;font-size:.9rem;margin-top:8px" id="launchText">Launch: current shell</p>
|
|
1460
1469
|
</div>
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
<
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
<select id="adapterType">
|
|
1468
|
-
<option value="anthropic">Anthropic Claude</option>
|
|
1469
|
-
<option value="openai">OpenAI GPT</option>
|
|
1470
|
-
<option value="cli">CLI Adapter</option>
|
|
1471
|
-
</select>
|
|
1472
|
-
<input type="text" id="model" placeholder="Model (optional)" />
|
|
1473
|
-
<input type="text" id="apiKey" placeholder="API Key (if not set in environment)" />
|
|
1474
|
-
<button onclick="configureAdapter()">Configure Adapter</button>
|
|
1475
|
-
</div>
|
|
1476
|
-
</div>
|
|
1477
|
-
|
|
1478
|
-
<div class="card chat-only" id="chatCard" style="display:none">
|
|
1479
|
-
<h2>Chat Messages</h2>
|
|
1480
|
-
<div class="messages" id="messages"></div>
|
|
1481
|
-
<div class="input-area chat-only">
|
|
1482
|
-
<textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
|
|
1483
|
-
<button onclick="sendMessage()">Send</button>
|
|
1484
|
-
</div>
|
|
1485
|
-
</div>
|
|
1470
|
+
|
|
1471
|
+
<div class="card pairing" id="pairingCard" style="display:none">
|
|
1472
|
+
<h2>Connect Your Phone</h2>
|
|
1473
|
+
<img id="qrCode" alt="QR Code"/>
|
|
1474
|
+
<div class="pairing-code" id="pairingCode"></div>
|
|
1475
|
+
<p style="color:#888;font-size:.85rem">Scan QR or enter code in viewer</p>
|
|
1486
1476
|
</div>
|
|
1487
|
-
|
|
1488
|
-
<!-- Terminal mode
|
|
1489
|
-
<div id="
|
|
1477
|
+
|
|
1478
|
+
<!-- Terminal mode: shell, Devin, Codex etc. (default when no AI adapter) -->
|
|
1479
|
+
<div id="terminalSection" style="display:none">
|
|
1490
1480
|
<div class="card">
|
|
1491
|
-
<h2>
|
|
1481
|
+
<h2>Launch Configuration</h2>
|
|
1492
1482
|
<div class="config-form">
|
|
1493
1483
|
<select id="cliPreset"></select>
|
|
1494
1484
|
<input type="text" id="command" placeholder="Custom launch command" style="display:none"/>
|
|
1495
1485
|
<p style="color:#666;font-size:.8rem;margin-top:4px" id="presetDesc"></p>
|
|
1496
|
-
<button
|
|
1486
|
+
<button id="applyLaunchBtn">Apply Launch Target</button>
|
|
1497
1487
|
</div>
|
|
1498
1488
|
</div>
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
<
|
|
1502
|
-
|
|
1503
|
-
<button class="tool-btn"
|
|
1504
|
-
<button class="tool-btn"
|
|
1505
|
-
<button class="tool-btn"
|
|
1506
|
-
<button class="tool-btn" onclick="sendTab()">Tab</button>
|
|
1489
|
+
<div class="card">
|
|
1490
|
+
<h2>Terminal</h2>
|
|
1491
|
+
<div class="toolbar">
|
|
1492
|
+
<button class="tool-btn" id="focusTermBtn">Focus</button>
|
|
1493
|
+
<button class="tool-btn" id="ctrlCBtn">Ctrl+C</button>
|
|
1494
|
+
<button class="tool-btn" id="escBtn">Esc</button>
|
|
1495
|
+
<button class="tool-btn" id="tabBtn">Tab</button>
|
|
1507
1496
|
</div>
|
|
1508
|
-
<div class="terminal-container
|
|
1497
|
+
<div class="terminal-container" id="terminalContainer">
|
|
1509
1498
|
<div id="terminal"></div>
|
|
1510
1499
|
</div>
|
|
1511
1500
|
</div>
|
|
1512
1501
|
</div>
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
<
|
|
1517
|
-
|
|
1518
|
-
|
|
1502
|
+
|
|
1503
|
+
<!-- Chat mode: direct LLM connections (Anthropic, OpenAI) -->
|
|
1504
|
+
<div id="chatSection" style="display:none">
|
|
1505
|
+
<div class="card">
|
|
1506
|
+
<h2>Chat</h2>
|
|
1507
|
+
<div class="messages" id="messages"></div>
|
|
1508
|
+
<div class="input-area">
|
|
1509
|
+
<textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
|
|
1510
|
+
<button id="sendBtn">Send</button>
|
|
1511
|
+
</div>
|
|
1512
|
+
</div>
|
|
1519
1513
|
</div>
|
|
1514
|
+
|
|
1520
1515
|
</div>
|
|
1521
|
-
<script>
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (window.Terminal) return;
|
|
1525
|
-
const [{ Terminal }, { FitAddon }] = await Promise.all([
|
|
1526
|
-
import('@xterm/xterm'),
|
|
1527
|
-
import('@xterm/addon-fit')
|
|
1528
|
-
]);
|
|
1529
|
-
window.Terminal = Terminal;
|
|
1530
|
-
window.FitAddon = FitAddon;
|
|
1531
|
-
};
|
|
1516
|
+
<script type="module">
|
|
1517
|
+
import { Terminal } from 'https://esm.sh/@xterm/xterm@6';
|
|
1518
|
+
import { FitAddon } from 'https://esm.sh/@xterm/addon-fit@0.11';
|
|
1532
1519
|
|
|
1533
|
-
const ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
|
1534
|
-
let cliPresets=[];
|
|
1535
|
-
let currentMode = null; // 'chat' or 'terminal'
|
|
1520
|
+
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
|
|
1536
1521
|
let term = null;
|
|
1537
1522
|
let fitAddon = null;
|
|
1523
|
+
let termInitialized = false;
|
|
1524
|
+
let cliPresets = [];
|
|
1538
1525
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
const chatMode = document.getElementById('chatMode');
|
|
1545
|
-
const terminalMode = document.getElementById('terminalMode');
|
|
1546
|
-
|
|
1547
|
-
if (mode === 'terminal') {
|
|
1548
|
-
chatMode.style.display = 'none';
|
|
1549
|
-
terminalMode.style.display = 'block';
|
|
1550
|
-
document.body.classList.add('terminal-mode');
|
|
1551
|
-
document.body.classList.remove('chat-mode');
|
|
1552
|
-
initTerminal();
|
|
1553
|
-
} else {
|
|
1554
|
-
chatMode.style.display = 'block';
|
|
1555
|
-
terminalMode.style.display = 'none';
|
|
1556
|
-
document.body.classList.add('chat-mode');
|
|
1557
|
-
document.body.classList.remove('terminal-mode');
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Terminal functionality
|
|
1562
|
-
async function initTerminal() {
|
|
1563
|
-
if (term) return;
|
|
1564
|
-
await loadTerminal();
|
|
1565
|
-
|
|
1566
|
-
term = new window.Terminal({
|
|
1526
|
+
function initTerminal() {
|
|
1527
|
+
if (termInitialized) return;
|
|
1528
|
+
termInitialized = true;
|
|
1529
|
+
term = new Terminal({
|
|
1567
1530
|
cursorBlink: true,
|
|
1568
|
-
fontFamily: 'ui-monospace,
|
|
1531
|
+
fontFamily: 'ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace',
|
|
1569
1532
|
fontSize: 14,
|
|
1570
1533
|
lineHeight: 1.25,
|
|
1571
|
-
allowTransparency: true,
|
|
1572
1534
|
scrollback: 5000,
|
|
1573
1535
|
theme: {
|
|
1574
|
-
background: '#05070c',
|
|
1575
|
-
foreground: '#e6edf3',
|
|
1576
|
-
cursor: '#7c8aff',
|
|
1577
|
-
cursorAccent: '#05070c',
|
|
1536
|
+
background: '#05070c', foreground: '#e6edf3', cursor: '#7c8aff', cursorAccent: '#05070c',
|
|
1578
1537
|
selectionBackground: 'rgba(124,138,255,0.28)',
|
|
1579
|
-
black: '#0a0d14',
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
blue: '#7c8aff',
|
|
1584
|
-
magenta: '#bc8cff',
|
|
1585
|
-
cyan: '#39c5cf',
|
|
1586
|
-
white: '#c9d1d9',
|
|
1587
|
-
brightBlack: '#6e7681',
|
|
1588
|
-
brightRed: '#ffa198',
|
|
1589
|
-
brightGreen: '#56d364',
|
|
1590
|
-
brightYellow: '#e3b341',
|
|
1591
|
-
brightBlue: '#a5b4ff',
|
|
1592
|
-
brightMagenta: '#d2a8ff',
|
|
1593
|
-
brightCyan: '#56d4dd',
|
|
1594
|
-
brightWhite: '#f0f6fc',
|
|
1538
|
+
black: '#0a0d14', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
|
|
1539
|
+
blue: '#7c8aff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#c9d1d9',
|
|
1540
|
+
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
|
|
1541
|
+
brightBlue: '#a5b4ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
|
|
1595
1542
|
},
|
|
1596
1543
|
});
|
|
1597
|
-
|
|
1598
|
-
fitAddon = new window.FitAddon();
|
|
1544
|
+
fitAddon = new FitAddon();
|
|
1599
1545
|
term.loadAddon(fitAddon);
|
|
1600
1546
|
term.open(document.getElementById('terminal'));
|
|
1601
|
-
|
|
1602
1547
|
term.onData((data) => {
|
|
1603
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1604
|
-
ws.send(JSON.stringify({ type: 'terminal_input', data }));
|
|
1605
|
-
}
|
|
1548
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data }));
|
|
1606
1549
|
});
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
fitAddon.fit();
|
|
1612
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1613
|
-
ws.send(JSON.stringify({
|
|
1614
|
-
type: 'terminal_resize',
|
|
1615
|
-
cols: term.cols,
|
|
1616
|
-
rows: term.rows
|
|
1617
|
-
}));
|
|
1618
|
-
}
|
|
1550
|
+
new ResizeObserver(() => {
|
|
1551
|
+
fitAddon.fit();
|
|
1552
|
+
if (ws.readyState === WebSocket.OPEN && term) {
|
|
1553
|
+
ws.send(JSON.stringify({ type: 'terminal_resize', cols: term.cols, rows: term.rows }));
|
|
1619
1554
|
}
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
// Auto-focus terminal
|
|
1624
|
-
term.focus();
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
function focusTerminal() {
|
|
1628
|
-
if (term) term.focus();
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
function sendCtrlC() {
|
|
1632
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1633
|
-
ws.send(JSON.stringify({ type: 'terminal_input', data: '' }));
|
|
1634
|
-
}
|
|
1635
|
-
if (term) term.focus();
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
function sendEsc() {
|
|
1639
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1640
|
-
ws.send(JSON.stringify({ type: 'terminal_input', data: '\x1B' }));
|
|
1641
|
-
}
|
|
1642
|
-
if (term) term.focus();
|
|
1555
|
+
}).observe(document.getElementById('terminalContainer'));
|
|
1556
|
+
if (ws.readyState === WebSocket.OPEN) sendTerminalOpen();
|
|
1643
1557
|
}
|
|
1644
1558
|
|
|
1645
|
-
function
|
|
1646
|
-
if (
|
|
1647
|
-
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1559
|
+
function sendTerminalOpen() {
|
|
1560
|
+
if (!term || !fitAddon) return;
|
|
1561
|
+
fitAddon.fit();
|
|
1562
|
+
ws.send(JSON.stringify({ type: 'terminal_open', cols: term.cols, rows: term.rows }));
|
|
1563
|
+
term.focus();
|
|
1650
1564
|
}
|
|
1651
1565
|
|
|
1652
|
-
|
|
1653
|
-
fetch('/api/cli-presets').then(r=>r.json()).then(presets=>{
|
|
1654
|
-
cliPresets=presets;
|
|
1655
|
-
const sel=document.getElementById('cliPreset');
|
|
1656
|
-
const shellOpt=document.createElement('option');
|
|
1657
|
-
shellOpt.value='shell';
|
|
1658
|
-
shellOpt.textContent='Shell (default)';
|
|
1659
|
-
sel.appendChild(shellOpt);
|
|
1660
|
-
presets.forEach(p=>{const o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o)});
|
|
1661
|
-
sel.addEventListener('change',()=>{
|
|
1662
|
-
const id=sel.value;
|
|
1663
|
-
const p=cliPresets.find(x=>x.id===id);
|
|
1664
|
-
const cmd=document.getElementById('command');
|
|
1665
|
-
if(id==='shell'){cmd.style.display='none';cmd.value='';document.getElementById('presetDesc').textContent='Start an interactive login shell. Run commands directly in the phone terminal.';return;}
|
|
1666
|
-
if(id==='custom'){cmd.style.display='';document.getElementById('presetDesc').textContent='Enter the exact launch command.';return;}
|
|
1667
|
-
cmd.style.display='none';
|
|
1668
|
-
document.getElementById('presetDesc').textContent=p?p.description:'';
|
|
1669
|
-
});
|
|
1670
|
-
sel.value='shell';
|
|
1671
|
-
sel.dispatchEvent(new Event('change'));
|
|
1672
|
-
return fetch('/api/config').then(r=>r.json());
|
|
1673
|
-
}).then(cfg=>{
|
|
1674
|
-
if(!cfg) return;
|
|
1675
|
-
const {saved}=cfg;
|
|
1676
|
-
if(saved && (saved.type==='terminal' || saved.type==='cli')){
|
|
1677
|
-
if(saved.preset) document.getElementById('cliPreset').value=saved.preset;
|
|
1678
|
-
if(saved.command) document.getElementById('command').value=saved.command;
|
|
1679
|
-
document.getElementById('cliPreset').dispatchEvent(new Event('change'));
|
|
1680
|
-
}
|
|
1681
|
-
}).catch(()=>{});
|
|
1566
|
+
ws.onopen = () => { if (termInitialized) sendTerminalOpen(); };
|
|
1682
1567
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
document.getElementById('
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
document.getElementById('
|
|
1693
|
-
}
|
|
1694
|
-
else if(d.type==='terminal_configured'){
|
|
1695
|
-
document.getElementById('launchText').textContent='Launch: '+d.terminalLaunch;
|
|
1696
|
-
document.getElementById('statusText').textContent='Launch target updated';
|
|
1697
|
-
}
|
|
1698
|
-
else if(d.type==='configured'){
|
|
1699
|
-
document.getElementById('statusText').textContent='AI adapter configured';
|
|
1700
|
-
}
|
|
1701
|
-
else if(d.type==='stream_chunk' && term){
|
|
1568
|
+
ws.onmessage = (e) => {
|
|
1569
|
+
const d = JSON.parse(e.data);
|
|
1570
|
+
if (d.type === 'peer_connected') {
|
|
1571
|
+
document.getElementById('dot').className = 'dot on';
|
|
1572
|
+
document.getElementById('statusText').textContent = 'Phone connected';
|
|
1573
|
+
} else if (d.type === 'peer_disconnected') {
|
|
1574
|
+
document.getElementById('dot').className = 'dot wait';
|
|
1575
|
+
document.getElementById('statusText').textContent = 'Phone disconnected';
|
|
1576
|
+
} else if (d.type === 'terminal_configured') {
|
|
1577
|
+
document.getElementById('launchText').textContent = 'Launch: ' + d.terminalLaunch;
|
|
1578
|
+
} else if (d.type === 'terminal_output' && term) {
|
|
1702
1579
|
term.write(d.data);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// Terminal stream ended
|
|
1706
|
-
}
|
|
1707
|
-
else if(d.type==='terminal_exit' && term){
|
|
1708
|
-
const detail = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
|
|
1580
|
+
} else if (d.type === 'terminal_exit' && term) {
|
|
1581
|
+
const code = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
|
|
1709
1582
|
term.writeln(\`\r
|
|
1710
|
-
[terminal \${
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1583
|
+
[terminal \${code}]\`);
|
|
1584
|
+
} else if (d.type === 'stream_chunk') {
|
|
1585
|
+
const msgs = document.getElementById('messages');
|
|
1586
|
+
if (msgs) {
|
|
1587
|
+
let last = msgs.lastElementChild;
|
|
1588
|
+
if (!last || last.dataset.streaming !== 'true') {
|
|
1589
|
+
last = document.createElement('div');
|
|
1590
|
+
last.className = 'msg assistant';
|
|
1591
|
+
last.dataset.streaming = 'true';
|
|
1592
|
+
msgs.appendChild(last);
|
|
1593
|
+
}
|
|
1594
|
+
last.textContent += d.data;
|
|
1595
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
1596
|
+
}
|
|
1597
|
+
} else if (d.type === 'stream_end') {
|
|
1598
|
+
const msgs = document.getElementById('messages');
|
|
1599
|
+
if (msgs?.lastElementChild) delete msgs.lastElementChild.dataset.streaming;
|
|
1600
|
+
} else if (d.type === 'message') {
|
|
1601
|
+
const msgs = document.getElementById('messages');
|
|
1602
|
+
if (msgs) {
|
|
1603
|
+
const el = document.createElement('div');
|
|
1604
|
+
el.className = \`msg \${d.role}\`;
|
|
1605
|
+
el.textContent = d.content;
|
|
1606
|
+
msgs.appendChild(el);
|
|
1607
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
1721
1608
|
}
|
|
1722
1609
|
}
|
|
1723
1610
|
};
|
|
1724
1611
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
if(d.
|
|
1728
|
-
|
|
1729
|
-
document.getElementById('
|
|
1730
|
-
document.getElementById('
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
document.getElementById('
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
switchToMode('terminal');
|
|
1612
|
+
fetch('/api/status').then(r => r.json()).then(d => {
|
|
1613
|
+
if (d.terminalLaunch) document.getElementById('launchText').textContent = 'Launch: ' + d.terminalLaunch;
|
|
1614
|
+
if (d.pairingCode) {
|
|
1615
|
+
document.getElementById('pairingCard').style.display = '';
|
|
1616
|
+
document.getElementById('pairingCode').textContent = d.pairingCode;
|
|
1617
|
+
if (d.pairingQR) document.getElementById('qrCode').src = d.pairingQR;
|
|
1618
|
+
}
|
|
1619
|
+
document.getElementById('dot').className = d.connected ? 'dot on' : 'dot wait';
|
|
1620
|
+
document.getElementById('statusText').textContent = d.connected ? 'Phone connected' : 'Waiting for phone...';
|
|
1621
|
+
if (d.adapter) {
|
|
1622
|
+
document.getElementById('chatSection').style.display = '';
|
|
1623
|
+
(d.messages || []).forEach(m => {
|
|
1624
|
+
const msgs = document.getElementById('messages');
|
|
1625
|
+
const el = document.createElement('div');
|
|
1626
|
+
el.className = \`msg \${m.role}\`;
|
|
1627
|
+
el.textContent = m.content;
|
|
1628
|
+
msgs.appendChild(el);
|
|
1629
|
+
});
|
|
1744
1630
|
} else {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
document.getElementById('chatCard').style.display = 'block';
|
|
1748
|
-
}
|
|
1631
|
+
document.getElementById('terminalSection').style.display = '';
|
|
1632
|
+
initTerminal();
|
|
1749
1633
|
}
|
|
1750
1634
|
});
|
|
1751
1635
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1636
|
+
fetch('/api/cli-presets').then(r => r.json()).then(presets => {
|
|
1637
|
+
cliPresets = presets;
|
|
1638
|
+
const sel = document.getElementById('cliPreset');
|
|
1639
|
+
const shellOpt = document.createElement('option');
|
|
1640
|
+
shellOpt.value = 'shell'; shellOpt.textContent = 'Shell (default)';
|
|
1641
|
+
sel.appendChild(shellOpt);
|
|
1642
|
+
presets.forEach(p => {
|
|
1643
|
+
const o = document.createElement('option');
|
|
1644
|
+
o.value = p.id; o.textContent = p.name;
|
|
1645
|
+
sel.appendChild(o);
|
|
1762
1646
|
});
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
document.getElementById('
|
|
1647
|
+
sel.addEventListener('change', () => {
|
|
1648
|
+
const id = sel.value;
|
|
1649
|
+
const p = cliPresets.find(x => x.id === id);
|
|
1650
|
+
const cmd = document.getElementById('command');
|
|
1651
|
+
const desc = document.getElementById('presetDesc');
|
|
1652
|
+
if (id === 'shell') { cmd.style.display = 'none'; cmd.value = ''; desc.textContent = 'Interactive login shell.'; return; }
|
|
1653
|
+
if (id === 'custom') { cmd.style.display = ''; desc.textContent = 'Enter the exact launch command.'; return; }
|
|
1654
|
+
cmd.style.display = 'none';
|
|
1655
|
+
desc.textContent = p ? p.description : '';
|
|
1656
|
+
});
|
|
1657
|
+
sel.value = 'shell'; sel.dispatchEvent(new Event('change'));
|
|
1658
|
+
return fetch('/api/config').then(r => r.json());
|
|
1659
|
+
}).then(cfg => {
|
|
1660
|
+
if (!cfg?.saved) return;
|
|
1661
|
+
const { saved } = cfg;
|
|
1662
|
+
if (saved.type === 'terminal' || saved.type === 'cli') {
|
|
1663
|
+
if (saved.preset) document.getElementById('cliPreset').value = saved.preset;
|
|
1664
|
+
if (saved.command) document.getElementById('command').value = saved.command;
|
|
1665
|
+
document.getElementById('cliPreset').dispatchEvent(new Event('change'));
|
|
1768
1666
|
}
|
|
1769
|
-
}
|
|
1667
|
+
}).catch(() => {});
|
|
1770
1668
|
|
|
1771
|
-
async
|
|
1669
|
+
document.getElementById('applyLaunchBtn').addEventListener('click', async () => {
|
|
1772
1670
|
const preset = document.getElementById('cliPreset').value;
|
|
1773
1671
|
const command = document.getElementById('command').value;
|
|
1774
|
-
|
|
1775
|
-
const
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1672
|
+
const r = await fetch('/api/configure', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'cli', preset, command }) });
|
|
1673
|
+
const d = await r.json();
|
|
1674
|
+
if (d.error) alert(d.error);
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
document.getElementById('focusTermBtn').addEventListener('click', () => term?.focus());
|
|
1678
|
+
document.getElementById('ctrlCBtn').addEventListener('click', () => {
|
|
1679
|
+
term?.focus();
|
|
1680
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: '' }));
|
|
1681
|
+
});
|
|
1682
|
+
document.getElementById('escBtn').addEventListener('click', () => {
|
|
1683
|
+
term?.focus();
|
|
1684
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: '\x1B' }));
|
|
1685
|
+
});
|
|
1686
|
+
document.getElementById('tabBtn').addEventListener('click', () => {
|
|
1687
|
+
term?.focus();
|
|
1688
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: ' ' }));
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
document.getElementById('sendBtn').addEventListener('click', sendMessage);
|
|
1692
|
+
document.getElementById('messageInput').addEventListener('keydown', (e) => {
|
|
1693
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
1694
|
+
});
|
|
1785
1695
|
|
|
1786
|
-
// Chat functionality
|
|
1787
1696
|
function sendMessage() {
|
|
1788
1697
|
const input = document.getElementById('messageInput');
|
|
1789
1698
|
const content = input.value.trim();
|
|
1790
1699
|
if (!content) return;
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1795
|
-
body: JSON.stringify({ content })
|
|
1796
|
-
}).then(() => {
|
|
1797
|
-
input.value = '';
|
|
1798
|
-
}).catch(err => {
|
|
1799
|
-
console.error('Failed to send message:', err);
|
|
1800
|
-
});
|
|
1700
|
+
fetch('/api/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) })
|
|
1701
|
+
.then(() => { input.value = ''; })
|
|
1702
|
+
.catch(err => console.error('Failed to send:', err));
|
|
1801
1703
|
}
|
|
1802
|
-
|
|
1803
|
-
// Handle Enter key in chat input
|
|
1804
|
-
document.getElementById('messageInput')?.addEventListener('keydown', (e) => {
|
|
1805
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1806
|
-
e.preventDefault();
|
|
1807
|
-
sendMessage();
|
|
1808
|
-
}
|
|
1809
|
-
});
|
|
1810
1704
|
</script>
|
|
1811
1705
|
</body>
|
|
1812
1706
|
</html>`;
|
|
1813
1707
|
|
|
1814
1708
|
// src/index.ts
|
|
1709
|
+
var QRCode = null;
|
|
1710
|
+
async function getQRCode() {
|
|
1711
|
+
if (!QRCode) QRCode = await import("qrcode");
|
|
1712
|
+
return QRCode;
|
|
1713
|
+
}
|
|
1714
|
+
console.log("[host] Module loaded");
|
|
1815
1715
|
function parseArgs(argv) {
|
|
1816
1716
|
const args = {};
|
|
1817
1717
|
const rest = argv.slice(2);
|
|
@@ -1873,6 +1773,7 @@ if (cliArgs.help) {
|
|
|
1873
1773
|
var DEFAULT_ABLY_KEY = "SfHSAQ.IRTOQQ:FBbi9a7ZV6jIu0Gdo_UeYhIN4rzpMrud5-LldURNh9s";
|
|
1874
1774
|
var DEFAULT_VIEWER_URL = "https://bobstrogg.github.io/Airloom/";
|
|
1875
1775
|
var VIEWER_URL = process.env.VIEWER_URL ?? DEFAULT_VIEWER_URL;
|
|
1776
|
+
var IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
|
|
1876
1777
|
var RELAY_URL = process.env.RELAY_URL;
|
|
1877
1778
|
var ABLY_API_KEY = process.env.ABLY_API_KEY ?? (RELAY_URL ? void 0 : DEFAULT_ABLY_KEY);
|
|
1878
1779
|
var ABLY_TOKEN_TTL = parseInt(process.env.ABLY_TOKEN_TTL ?? String(24 * 60 * 60 * 1e3), 10);
|
|
@@ -1968,7 +1869,10 @@ async function main() {
|
|
|
1968
1869
|
connected: false,
|
|
1969
1870
|
terminalLaunch,
|
|
1970
1871
|
terminalLaunchCommand: launchCommand,
|
|
1971
|
-
messages: []
|
|
1872
|
+
messages: [],
|
|
1873
|
+
sessionToken: session.sessionToken,
|
|
1874
|
+
ablyToken: useAbly ? pairingData.token : void 0,
|
|
1875
|
+
transport: useAbly ? "ably" : "ws"
|
|
1972
1876
|
};
|
|
1973
1877
|
console.log(`[host] Terminal launch: ${terminalLaunch}`);
|
|
1974
1878
|
if (cliArgs.cli || cliArgs.preset) {
|
|
@@ -2034,31 +1938,37 @@ async function main() {
|
|
|
2034
1938
|
const { server, broadcast, port } = await createHostServer({ port: HOST_PORT, state, viewerDir });
|
|
2035
1939
|
const pairingBase64 = Buffer.from(pairingJSON).toString("base64url");
|
|
2036
1940
|
const viewerBase = VIEWER_URL.replace(/\/+$/, "");
|
|
2037
|
-
const
|
|
1941
|
+
const pagesUrl = `${viewerBase}/#${pairingBase64}`;
|
|
2038
1942
|
const lanIP = getLanIP();
|
|
2039
1943
|
const lanHost = lanIP ?? "localhost";
|
|
2040
1944
|
const lanBaseUrl = `http://${lanHost}:${port}`;
|
|
2041
1945
|
const lanViewerUrl = viewerDir ? `${lanBaseUrl}/viewer/#${pairingBase64}` : null;
|
|
2042
|
-
const
|
|
2043
|
-
const
|
|
1946
|
+
const qrTarget = IS_DEV && lanViewerUrl ? lanViewerUrl : pagesUrl;
|
|
1947
|
+
const qrcode = await getQRCode();
|
|
1948
|
+
const qrDataUrl = await qrcode.toDataURL(qrTarget, { width: 300, margin: 2 });
|
|
1949
|
+
const qrTerminal = await qrcode.toString(qrTarget, { type: "terminal", small: true });
|
|
2044
1950
|
state.pairingQR = qrDataUrl;
|
|
2045
1951
|
console.log("\nPairing QR Code:");
|
|
2046
1952
|
console.log(qrTerminal);
|
|
2047
1953
|
console.log(`Pairing Code: ${displayCode}`);
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
console.log(`
|
|
1954
|
+
if (IS_DEV && lanViewerUrl) {
|
|
1955
|
+
console.log(`Viewer URL (LAN/dev): ${lanViewerUrl}`);
|
|
1956
|
+
console.log(`Pages URL: ${pagesUrl}`);
|
|
1957
|
+
} else {
|
|
1958
|
+
console.log(`Viewer URL: ${pagesUrl}`);
|
|
1959
|
+
if (lanViewerUrl) console.log(`LAN Viewer: ${lanViewerUrl}`);
|
|
2051
1960
|
}
|
|
2052
1961
|
if (!useAbly) console.log(`Relay: ${RELAY_URL}`);
|
|
2053
1962
|
const localUrl = `http://localhost:${port}`;
|
|
2054
1963
|
console.log(`[host] Web UI at ${localUrl}
|
|
2055
1964
|
`);
|
|
2056
|
-
import("child_process").then(({ exec }) => {
|
|
1965
|
+
import("node:child_process").then(({ exec }) => {
|
|
2057
1966
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2058
1967
|
exec(`${cmd} ${localUrl}`);
|
|
2059
1968
|
}).catch(() => {
|
|
2060
1969
|
});
|
|
2061
|
-
const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand);
|
|
1970
|
+
const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand, broadcast);
|
|
1971
|
+
state.terminal = terminal;
|
|
2062
1972
|
channel.on("ready", () => {
|
|
2063
1973
|
console.log("[host] Phone connected! Channel ready.");
|
|
2064
1974
|
state.connected = true;
|