@bakapiano/ccsm 0.18.1 → 0.18.3
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/lib/tunnel.js +39 -30
- package/lib/webTerminal.js +11 -0
- package/package.json +1 -1
- package/public/css/terminals.css +54 -0
- package/public/css/widgets.css +21 -0
- package/public/js/components/TerminalView.js +63 -5
- package/public/js/pages/RemotePage.js +18 -0
- package/server.js +10 -10
package/lib/tunnel.js
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
// dirs. Returns the absolute path so we can spawn the child regardless
|
|
18
18
|
// of whether the post-install PATH refresh has reached this Node process.
|
|
19
19
|
|
|
20
|
-
const { spawn,
|
|
20
|
+
const { spawn, execFile } = require('node:child_process');
|
|
21
21
|
const path = require('node:path');
|
|
22
22
|
const fs = require('node:fs');
|
|
23
23
|
const os = require('node:os');
|
|
24
|
+
const { promisify } = require('node:util');
|
|
25
|
+
const execFileP = promisify(execFile);
|
|
24
26
|
|
|
25
27
|
const PROVIDERS = {
|
|
26
28
|
cloudflared: {
|
|
@@ -61,14 +63,15 @@ let token = null; // Remote-access bearer token. Null = no remote
|
|
|
61
63
|
function getToken() { return token; }
|
|
62
64
|
function setToken(t) { token = t ? String(t) : null; return token; }
|
|
63
65
|
|
|
64
|
-
function findBinary(provider) {
|
|
66
|
+
async function findBinary(provider) {
|
|
65
67
|
const p = PROVIDERS[provider];
|
|
66
68
|
if (!p) return null;
|
|
67
69
|
// PATH lookup via where.exe — works regardless of how the CLI got
|
|
68
|
-
// installed (winget, choco, manual, in-tree).
|
|
70
|
+
// installed (winget, choco, manual, in-tree). windowsHide stops the
|
|
71
|
+
// conhost window from flashing.
|
|
69
72
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
73
|
+
const { stdout } = await execFileP('where.exe', [p.binary], { windowsHide: true });
|
|
74
|
+
const out = String(stdout).trim().split(/\r?\n/)[0];
|
|
72
75
|
if (out && fs.existsSync(out)) return out;
|
|
73
76
|
} catch { /* not on PATH */ }
|
|
74
77
|
// Fall back to known install locations (winget's PATH update doesn't
|
|
@@ -92,20 +95,18 @@ function findBinary(provider) {
|
|
|
92
95
|
return null;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
function getVersion(provider, exe) {
|
|
98
|
+
async function getVersion(provider, exe) {
|
|
96
99
|
try {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
return out || null;
|
|
100
|
+
const { stdout } = await execFileP(exe, ['--version'], { windowsHide: true });
|
|
101
|
+
return String(stdout).trim().split(/\r?\n/)[0] || null;
|
|
100
102
|
} catch { return null; }
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
function checkDevtunnelLogin(exe) {
|
|
105
|
+
async function checkDevtunnelLogin(exe) {
|
|
104
106
|
try {
|
|
105
|
-
const
|
|
106
|
-
.toString().trim();
|
|
107
|
+
const { stdout } = await execFileP(exe, ['user', 'show'], { windowsHide: true, timeout: 5000 });
|
|
107
108
|
// "Logged in as <email> using <provider>." vs "Not logged in"
|
|
108
|
-
const m =
|
|
109
|
+
const m = String(stdout).trim().match(/Logged in as (\S+)/);
|
|
109
110
|
if (m) return { loggedIn: true, user: m[1] };
|
|
110
111
|
return { loggedIn: false, user: null };
|
|
111
112
|
} catch {
|
|
@@ -123,31 +124,39 @@ const PROBE_TTL_MS = 30_000;
|
|
|
123
124
|
let probeCache = null;
|
|
124
125
|
let probeCacheAt = 0;
|
|
125
126
|
|
|
126
|
-
function probe(force = false) {
|
|
127
|
+
async function probe(force = false) {
|
|
127
128
|
if (!force && probeCache && Date.now() - probeCacheAt < PROBE_TTL_MS) {
|
|
128
129
|
return probeCache;
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// All providers in parallel; within each, --version and the
|
|
132
|
+
// devtunnel `user show` check run together too. Cold probe drops
|
|
133
|
+
// from ~1.5s serial to ~700ms (capped by the slowest exec —
|
|
134
|
+
// typically `devtunnel user show`).
|
|
135
|
+
const ids = Object.keys(PROVIDERS);
|
|
136
|
+
const results = await Promise.all(ids.map(async (id) => {
|
|
137
|
+
const exe = await findBinary(id);
|
|
138
|
+
const p = { installed: !!exe, exe, version: null };
|
|
139
|
+
if (exe) {
|
|
140
|
+
const tasks = [getVersion(id, exe)];
|
|
141
|
+
if (id === 'devtunnel') tasks.push(checkDevtunnelLogin(exe));
|
|
142
|
+
const [version, devUser] = await Promise.all(tasks);
|
|
143
|
+
p.version = version;
|
|
144
|
+
if (devUser) Object.assign(p, devUser);
|
|
136
145
|
}
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
probeCache =
|
|
146
|
+
return [id, p];
|
|
147
|
+
}));
|
|
148
|
+
probeCache = Object.fromEntries(results);
|
|
140
149
|
probeCacheAt = Date.now();
|
|
141
|
-
return
|
|
150
|
+
return probeCache;
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
// Invalidate the cache when callers know the on-disk state likely changed
|
|
145
154
|
// (post-install, post-login, etc.). Next probe() re-shells.
|
|
146
155
|
function invalidateProbe() { probeCache = null; probeCacheAt = 0; }
|
|
147
156
|
|
|
148
|
-
function status() {
|
|
157
|
+
async function status() {
|
|
149
158
|
return {
|
|
150
|
-
providers: probe(),
|
|
159
|
+
providers: await probe(),
|
|
151
160
|
running: !!current,
|
|
152
161
|
provider: current?.provider || null,
|
|
153
162
|
url: current?.url || null,
|
|
@@ -169,10 +178,10 @@ async function start({ provider, port }) {
|
|
|
169
178
|
if (current) throw new Error('tunnel already running');
|
|
170
179
|
const p = PROVIDERS[provider];
|
|
171
180
|
if (!p) throw new Error(`unknown provider: ${provider}`);
|
|
172
|
-
const exe = findBinary(provider);
|
|
181
|
+
const exe = await findBinary(provider);
|
|
173
182
|
if (!exe) throw new Error(`${p.label} is not installed`);
|
|
174
183
|
if (provider === 'devtunnel') {
|
|
175
|
-
const { loggedIn } = checkDevtunnelLogin(exe);
|
|
184
|
+
const { loggedIn } = await checkDevtunnelLogin(exe);
|
|
176
185
|
if (!loggedIn) throw new Error('devtunnel requires login — run `devtunnel user login` first');
|
|
177
186
|
}
|
|
178
187
|
|
|
@@ -206,7 +215,7 @@ async function start({ provider, port }) {
|
|
|
206
215
|
// Wait up to 25s for the URL to show up in stdout.
|
|
207
216
|
const deadline = Date.now() + 25_000;
|
|
208
217
|
while (Date.now() < deadline) {
|
|
209
|
-
if (entry.url) return status();
|
|
218
|
+
if (entry.url) return await status();
|
|
210
219
|
if (!current || current !== entry) {
|
|
211
220
|
throw new Error('tunnel exited before reporting a URL · ' + entry.log.slice(-3).join(' / '));
|
|
212
221
|
}
|
|
@@ -214,7 +223,7 @@ async function start({ provider, port }) {
|
|
|
214
223
|
}
|
|
215
224
|
// Timed out — keep the child alive (the URL might appear later) but
|
|
216
225
|
// tell the caller we don't have one yet.
|
|
217
|
-
return status();
|
|
226
|
+
return await status();
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
function stop() {
|
package/lib/webTerminal.js
CHANGED
|
@@ -138,6 +138,17 @@ function attach(id, ws) {
|
|
|
138
138
|
try { ws.close(4404, 'no such terminal'); } catch {}
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
|
+
// Latest-wins: a session has at most one live WebSocket at any
|
|
142
|
+
// moment. The new attach displaces any existing ones — keeps PTY
|
|
143
|
+
// resize semantics unambiguous (each client sends its own dimensions
|
|
144
|
+
// for its own viewport; with two clients fighting, claude's TUI
|
|
145
|
+
// re-renders for whichever sent last and the other side sees a
|
|
146
|
+
// mis-sized layout). Close code 4001 + reason lets the displaced
|
|
147
|
+
// client show a clearer message than the generic "[disconnected]".
|
|
148
|
+
for (const other of entry.sockets) {
|
|
149
|
+
try { other.close(4001, 'displaced by another client'); } catch {}
|
|
150
|
+
}
|
|
151
|
+
entry.sockets.clear();
|
|
141
152
|
entry.sockets.add(ws);
|
|
142
153
|
if (entry.history) {
|
|
143
154
|
try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history) })); } catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
package/public/css/terminals.css
CHANGED
|
@@ -440,3 +440,57 @@
|
|
|
440
440
|
background: #faf9f5;
|
|
441
441
|
border-color: #faf9f5;
|
|
442
442
|
}
|
|
443
|
+
|
|
444
|
+
/* Displaced state — shown when the server kicks us off because another
|
|
445
|
+
client attached to the same session (latest-wins). Same dark surface
|
|
446
|
+
as terminal-empty so the transition from running terminal → displaced
|
|
447
|
+
doesn't flash a colour change. */
|
|
448
|
+
.terminal-displaced {
|
|
449
|
+
background: #1a1815;
|
|
450
|
+
color: #e8e3d5;
|
|
451
|
+
display: flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
justify-content: center;
|
|
454
|
+
height: 100%;
|
|
455
|
+
padding: var(--s-5);
|
|
456
|
+
}
|
|
457
|
+
.terminal-displaced-card {
|
|
458
|
+
max-width: 460px;
|
|
459
|
+
text-align: center;
|
|
460
|
+
display: flex;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
gap: var(--s-3);
|
|
463
|
+
}
|
|
464
|
+
.terminal-displaced-card h2 {
|
|
465
|
+
margin: 0;
|
|
466
|
+
font-size: 16px;
|
|
467
|
+
font-weight: 600;
|
|
468
|
+
color: #faf9f5;
|
|
469
|
+
letter-spacing: -0.005em;
|
|
470
|
+
}
|
|
471
|
+
.terminal-displaced-card p {
|
|
472
|
+
margin: 0;
|
|
473
|
+
font-size: 13px;
|
|
474
|
+
line-height: 1.55;
|
|
475
|
+
color: rgba(232, 227, 213, 0.72);
|
|
476
|
+
}
|
|
477
|
+
.terminal-displaced-actions {
|
|
478
|
+
margin-top: var(--s-2);
|
|
479
|
+
display: flex;
|
|
480
|
+
justify-content: center;
|
|
481
|
+
}
|
|
482
|
+
.terminal-displaced-card .action.primary {
|
|
483
|
+
background: #e8e3d5;
|
|
484
|
+
color: #1a1815;
|
|
485
|
+
border-color: #e8e3d5;
|
|
486
|
+
padding: 9px 20px;
|
|
487
|
+
font-size: 13px;
|
|
488
|
+
}
|
|
489
|
+
.terminal-displaced-card .action.primary:hover {
|
|
490
|
+
background: #faf9f5;
|
|
491
|
+
border-color: #faf9f5;
|
|
492
|
+
}
|
|
493
|
+
.terminal-displaced-hint {
|
|
494
|
+
font-size: 11.5px !important;
|
|
495
|
+
color: rgba(232, 227, 213, 0.45) !important;
|
|
496
|
+
}
|
package/public/css/widgets.css
CHANGED
|
@@ -1724,6 +1724,27 @@
|
|
|
1724
1724
|
word-break: break-all;
|
|
1725
1725
|
}
|
|
1726
1726
|
|
|
1727
|
+
.remote-loading {
|
|
1728
|
+
display: flex;
|
|
1729
|
+
flex-direction: column;
|
|
1730
|
+
align-items: center;
|
|
1731
|
+
justify-content: center;
|
|
1732
|
+
gap: var(--s-3);
|
|
1733
|
+
min-height: 260px;
|
|
1734
|
+
color: var(--ink-muted);
|
|
1735
|
+
font-size: 13px;
|
|
1736
|
+
}
|
|
1737
|
+
.remote-loading p { margin: 0; }
|
|
1738
|
+
.remote-loading-spinner {
|
|
1739
|
+
width: 28px;
|
|
1740
|
+
height: 28px;
|
|
1741
|
+
border-radius: 50%;
|
|
1742
|
+
border: 2px solid var(--border-strong);
|
|
1743
|
+
border-top-color: var(--ink);
|
|
1744
|
+
animation: remote-spin 0.75s linear infinite;
|
|
1745
|
+
}
|
|
1746
|
+
@keyframes remote-spin { to { transform: rotate(360deg); } }
|
|
1747
|
+
|
|
1727
1748
|
.remote-empty {
|
|
1728
1749
|
margin: 0;
|
|
1729
1750
|
font-size: 12.5px;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// output frames into xterm. Disposes everything on unmount or id change.
|
|
4
4
|
|
|
5
5
|
import { html } from '../html.js';
|
|
6
|
-
import { useEffect, useRef } from 'preact/hooks';
|
|
6
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
7
7
|
import { Terminal } from '@xterm/xterm';
|
|
8
8
|
import { FitAddon } from '@xterm/addon-fit';
|
|
9
9
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
@@ -36,6 +36,13 @@ export function TerminalView({ terminalId }) {
|
|
|
36
36
|
const hostRef = useRef(null);
|
|
37
37
|
const termRef = useRef(null);
|
|
38
38
|
const wsRef = useRef(null);
|
|
39
|
+
// Set when ws.onclose receives our custom "displaced by another
|
|
40
|
+
// client" code (4001) from lib/webTerminal.js's latest-wins policy.
|
|
41
|
+
// Renders a full-pane prompt with a "Take it back" button that bumps
|
|
42
|
+
// reattachNonce → useEffect re-runs → new WS, displacing whoever
|
|
43
|
+
// currently holds the session.
|
|
44
|
+
const [displaced, setDisplaced] = useState(false);
|
|
45
|
+
const [reattachNonce, setReattach] = useState(0);
|
|
39
46
|
|
|
40
47
|
useEffect(() => {
|
|
41
48
|
if (!terminalId || !hostRef.current) return;
|
|
@@ -148,8 +155,19 @@ export function TerminalView({ terminalId }) {
|
|
|
148
155
|
term.write(`\r\n\x1b[2m[process exited · code ${frame.code}]\x1b[0m\r\n`);
|
|
149
156
|
}
|
|
150
157
|
};
|
|
151
|
-
ws.onclose = () => {
|
|
152
|
-
|
|
158
|
+
ws.onclose = (ev) => {
|
|
159
|
+
// Server uses code 4001 + reason "displaced by another client"
|
|
160
|
+
// when a fresh attach takes over the session (latest-wins policy
|
|
161
|
+
// in lib/webTerminal.js's attach). We replace the terminal with
|
|
162
|
+
// a full-pane prompt + Take it back button via setDisplaced(true).
|
|
163
|
+
// Generic disconnects (network blip, server restart, PTY exit)
|
|
164
|
+
// get the dim inline notice as before — those usually self-heal
|
|
165
|
+
// and aren't worth a modal.
|
|
166
|
+
if (ev && ev.code === 4001) {
|
|
167
|
+
setDisplaced(true);
|
|
168
|
+
} else {
|
|
169
|
+
term.write('\r\n\x1b[2m[disconnected]\x1b[0m\r\n');
|
|
170
|
+
}
|
|
153
171
|
};
|
|
154
172
|
|
|
155
173
|
const onData = (data) => {
|
|
@@ -328,10 +346,50 @@ export function TerminalView({ terminalId }) {
|
|
|
328
346
|
termRef.current = null;
|
|
329
347
|
wsRef.current = null;
|
|
330
348
|
};
|
|
331
|
-
}, [terminalId]);
|
|
349
|
+
}, [terminalId, reattachNonce]);
|
|
332
350
|
|
|
333
351
|
if (!terminalId) {
|
|
334
352
|
return html`<div class="terminal-empty">Select a terminal on the left, or launch a new one.</div>`;
|
|
335
353
|
}
|
|
336
|
-
|
|
354
|
+
if (displaced) {
|
|
355
|
+
// Distinct key (and a non-div tag) forces Preact's reconciler to
|
|
356
|
+
// UNMOUNT the host <div> and mount a fresh element. Without this,
|
|
357
|
+
// Preact reuses the same DOM node, only flipping its className —
|
|
358
|
+
// and the xterm canvases stay parented inside, visible behind our
|
|
359
|
+
// overlay text. Re-mount on reattach: bumping reattachNonce reruns
|
|
360
|
+
// the effect with a fresh Terminal + WebSocket pair, which the
|
|
361
|
+
// server's latest-wins gate handles by displacing the current
|
|
362
|
+
// holder.
|
|
363
|
+
return html`
|
|
364
|
+
<section key="displaced" class="terminal-displaced">
|
|
365
|
+
<div class="terminal-displaced-card">
|
|
366
|
+
<h2>Another device picked up this session</h2>
|
|
367
|
+
<p>
|
|
368
|
+
Only one client at a time can attach. Your terminal here was
|
|
369
|
+
closed when another browser opened this session — its keystrokes
|
|
370
|
+
and resize events would otherwise fight yours.
|
|
371
|
+
</p>
|
|
372
|
+
<div class="terminal-displaced-actions">
|
|
373
|
+
<button class="action primary"
|
|
374
|
+
onClick=${() => {
|
|
375
|
+
// Clear displaced FIRST so the next render swaps the
|
|
376
|
+
// overlay out for the host div — that's the only
|
|
377
|
+
// way hostRef.current populates. Then bump the nonce
|
|
378
|
+
// so the effect re-runs with the freshly-mounted
|
|
379
|
+
// host and opens a new WS. Doing both in one tick
|
|
380
|
+
// batches the state updates; React renders, mounts
|
|
381
|
+
// the host, then flushes effects.
|
|
382
|
+
setDisplaced(false);
|
|
383
|
+
setReattach((n) => n + 1);
|
|
384
|
+
}}>
|
|
385
|
+
Take it back
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
<p class="terminal-displaced-hint">
|
|
389
|
+
Taking it back will close the other client the same way.
|
|
390
|
+
</p>
|
|
391
|
+
</div>
|
|
392
|
+
</section>`;
|
|
393
|
+
}
|
|
394
|
+
return html`<div key="host" ref=${hostRef} class="terminal-host"></div>`;
|
|
337
395
|
}
|
|
@@ -124,6 +124,12 @@ export function RemotePage() {
|
|
|
124
124
|
const [token, setTokenLocal] = useState('');
|
|
125
125
|
const [busy, setBusy] = useState(false);
|
|
126
126
|
const [deviceList, setDeviceList] = useState([]);
|
|
127
|
+
// First /api/tunnel/status round-trip is the slow one — even with
|
|
128
|
+
// 30s server-side cache + parallel probe, a cold call shells out to
|
|
129
|
+
// where.exe / --version / `devtunnel user show` and adds ~700ms.
|
|
130
|
+
// We hide the form behind a spinner during that window so the user
|
|
131
|
+
// doesn't see empty radios + placeholders that suddenly fill in.
|
|
132
|
+
const [loading, setLoading] = useState(true);
|
|
127
133
|
const pollRef = useRef(null);
|
|
128
134
|
|
|
129
135
|
async function refresh() {
|
|
@@ -143,6 +149,7 @@ export function RemotePage() {
|
|
|
143
149
|
return cur || 'cloudflared';
|
|
144
150
|
});
|
|
145
151
|
} catch (e) { setToast(`status load failed · ${e.message}`, 'error'); }
|
|
152
|
+
finally { setLoading(false); }
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
useEffect(() => {
|
|
@@ -245,6 +252,17 @@ export function RemotePage() {
|
|
|
245
252
|
const cf = status?.providers?.cloudflared;
|
|
246
253
|
const dt = status?.providers?.devtunnel;
|
|
247
254
|
|
|
255
|
+
if (loading) {
|
|
256
|
+
return html`
|
|
257
|
+
<${PageTitleBar} title="Remote" />
|
|
258
|
+
<div class="settings-scroll">
|
|
259
|
+
<div class="remote-loading">
|
|
260
|
+
<div class="remote-loading-spinner" aria-hidden="true"></div>
|
|
261
|
+
<p>Probing tunnel providers…</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>`;
|
|
264
|
+
}
|
|
265
|
+
|
|
248
266
|
return html`
|
|
249
267
|
<${PageTitleBar} title="Remote" />
|
|
250
268
|
<div class="settings-scroll">
|
package/server.js
CHANGED
|
@@ -1035,9 +1035,9 @@ app.post('/api/shutdown', (_req, res) => {
|
|
|
1035
1035
|
// the chosen tunnel CLI. URL appears asynchronously in the CLI's
|
|
1036
1036
|
// stdout; lib/tunnel parses it. /status returns the latest snapshot
|
|
1037
1037
|
// for the page to poll.
|
|
1038
|
-
app.get('/api/tunnel/status', (_req, res) => {
|
|
1039
|
-
res.json(tunnel.status());
|
|
1040
|
-
});
|
|
1038
|
+
app.get('/api/tunnel/status', asyncH(async (_req, res) => {
|
|
1039
|
+
res.json(await tunnel.status());
|
|
1040
|
+
}));
|
|
1041
1041
|
app.post('/api/tunnel/start', asyncH(async (req, res) => {
|
|
1042
1042
|
const { provider, token } = req.body || {};
|
|
1043
1043
|
if (!token || String(token).length < 8) {
|
|
@@ -1048,20 +1048,20 @@ app.post('/api/tunnel/start', asyncH(async (req, res) => {
|
|
|
1048
1048
|
const result = await tunnel.start({ provider, port: currentPort });
|
|
1049
1049
|
res.json(result);
|
|
1050
1050
|
} catch (e) {
|
|
1051
|
-
res.status(400).json({ error: e.message, providers: tunnel.probe() });
|
|
1051
|
+
res.status(400).json({ error: e.message, providers: await tunnel.probe().catch(() => ({})) });
|
|
1052
1052
|
}
|
|
1053
1053
|
}));
|
|
1054
|
-
app.post('/api/tunnel/stop', (_req, res) => {
|
|
1054
|
+
app.post('/api/tunnel/stop', asyncH(async (_req, res) => {
|
|
1055
1055
|
const stopped = tunnel.stop();
|
|
1056
|
-
res.json({ stopped, ...tunnel.status() });
|
|
1057
|
-
});
|
|
1058
|
-
app.post('/api/tunnel/token', (req, res) => {
|
|
1056
|
+
res.json({ stopped, ...(await tunnel.status()) });
|
|
1057
|
+
}));
|
|
1058
|
+
app.post('/api/tunnel/token', asyncH(async (req, res) => {
|
|
1059
1059
|
// Bare token update without touching the running tunnel.
|
|
1060
1060
|
// POST { token: '' } to clear and disable remote auth.
|
|
1061
1061
|
const t = (req.body && req.body.token) || '';
|
|
1062
1062
|
tunnel.setToken(t);
|
|
1063
|
-
res.json(tunnel.status());
|
|
1064
|
-
});
|
|
1063
|
+
res.json(await tunnel.status());
|
|
1064
|
+
}));
|
|
1065
1065
|
app.post('/api/tunnel/install', asyncH(async (req, res) => {
|
|
1066
1066
|
const { provider } = req.body || {};
|
|
1067
1067
|
try {
|