@bradygaster/squad-cli 0.8.22 → 0.8.24
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/cli/commands/rc.d.ts.map +1 -1
- package/dist/cli/commands/rc.js +10 -3
- package/dist/cli/commands/rc.js.map +1 -1
- package/dist/cli/commands/streams.d.ts +10 -8
- package/dist/cli/commands/streams.d.ts.map +1 -1
- package/dist/cli/commands/streams.js +51 -49
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli-entry.js +50 -14
- package/dist/cli-entry.js.map +1 -1
- package/dist/remote-ui/app.js +334 -0
- package/dist/remote-ui/index.html +57 -0
- package/dist/remote-ui/manifest.json +10 -0
- package/dist/remote-ui/styles.css +249 -0
- package/package.json +9 -2
- package/scripts/patch-esm-imports.mjs +77 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squad Remote Control — PTY Terminal PWA
|
|
3
|
+
* Raw terminal rendering via xterm.js + WebSocket
|
|
4
|
+
*/
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
let ws = null;
|
|
9
|
+
let connected = false;
|
|
10
|
+
let replaying = false;
|
|
11
|
+
let reconnectDelay = 1000;
|
|
12
|
+
|
|
13
|
+
const $ = (sel) => document.querySelector(sel);
|
|
14
|
+
const terminal = $('#terminal');
|
|
15
|
+
const inputEl = $('#input');
|
|
16
|
+
const formEl = $('#input-form');
|
|
17
|
+
const statusEl = $('#status-indicator');
|
|
18
|
+
const statusText = $('#status-text');
|
|
19
|
+
const permOverlay = $('#permission-overlay');
|
|
20
|
+
const dashboard = $('#dashboard');
|
|
21
|
+
const termContainer = $('#terminal-container');
|
|
22
|
+
let currentView = 'terminal'; // 'dashboard' or 'terminal'
|
|
23
|
+
|
|
24
|
+
// ─── xterm.js Terminal ───────────────────────────────────
|
|
25
|
+
let xterm = null;
|
|
26
|
+
let fitAddon = null;
|
|
27
|
+
|
|
28
|
+
function initXterm() {
|
|
29
|
+
if (xterm) return;
|
|
30
|
+
xterm = new Terminal({
|
|
31
|
+
theme: {
|
|
32
|
+
background: '#0d1117',
|
|
33
|
+
foreground: '#c9d1d9',
|
|
34
|
+
cursor: '#3fb950',
|
|
35
|
+
selectionBackground: '#264f78',
|
|
36
|
+
black: '#0d1117',
|
|
37
|
+
red: '#f85149',
|
|
38
|
+
green: '#3fb950',
|
|
39
|
+
yellow: '#d29922',
|
|
40
|
+
blue: '#58a6ff',
|
|
41
|
+
magenta: '#bc8cff',
|
|
42
|
+
cyan: '#39c5cf',
|
|
43
|
+
white: '#c9d1d9',
|
|
44
|
+
brightBlack: '#6e7681',
|
|
45
|
+
brightRed: '#f85149',
|
|
46
|
+
brightGreen: '#3fb950',
|
|
47
|
+
brightYellow: '#d29922',
|
|
48
|
+
brightBlue: '#58a6ff',
|
|
49
|
+
brightMagenta: '#bc8cff',
|
|
50
|
+
brightCyan: '#39c5cf',
|
|
51
|
+
brightWhite: '#f0f6fc',
|
|
52
|
+
},
|
|
53
|
+
fontFamily: "'Cascadia Code', 'SF Mono', 'Fira Code', 'Menlo', monospace",
|
|
54
|
+
fontSize: 13,
|
|
55
|
+
scrollback: 5000,
|
|
56
|
+
cursorBlink: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
fitAddon = new FitAddon.FitAddon();
|
|
60
|
+
xterm.loadAddon(fitAddon);
|
|
61
|
+
xterm.open(termContainer);
|
|
62
|
+
fitAddon.fit();
|
|
63
|
+
|
|
64
|
+
// Send terminal size to PTY so copilot renders correctly
|
|
65
|
+
function sendResize() {
|
|
66
|
+
if (ws && ws.readyState === WebSocket.OPEN && xterm) {
|
|
67
|
+
ws.send(JSON.stringify({ type: 'pty_resize', cols: xterm.cols, rows: xterm.rows }));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle resize
|
|
72
|
+
window.addEventListener('resize', () => {
|
|
73
|
+
if (fitAddon) { fitAddon.fit(); sendResize(); }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Send initial size
|
|
77
|
+
setTimeout(sendResize, 500);
|
|
78
|
+
|
|
79
|
+
// Keyboard input → send to bridge → PTY
|
|
80
|
+
xterm.onData((data) => {
|
|
81
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
82
|
+
ws.send(JSON.stringify({ type: 'pty_input', data }));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Dashboard ───────────────────────────────────────────
|
|
88
|
+
let showOffline = false;
|
|
89
|
+
|
|
90
|
+
async function loadSessions() {
|
|
91
|
+
try {
|
|
92
|
+
const resp = await fetch('/api/sessions');
|
|
93
|
+
const data = await resp.json();
|
|
94
|
+
renderDashboard(data.sessions || []);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
dashboard.innerHTML = '<div style="padding:12px;color:var(--red)">' + escapeHtml('Failed to load sessions: ' + err.message) + '</div>';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderDashboard(sessions) {
|
|
101
|
+
const filtered = showOffline ? sessions : sessions.filter(s => s.online);
|
|
102
|
+
const offlineCount = sessions.filter(s => !s.online).length;
|
|
103
|
+
const onlineCount = sessions.filter(s => s.online).length;
|
|
104
|
+
|
|
105
|
+
let html = `<div style="padding:8px 4px;display:flex;align-items:center;gap:8px">
|
|
106
|
+
<span style="color:var(--text-dim);font-size:12px">${onlineCount} online${offlineCount > 0 ? ', ' + offlineCount + ' offline' : ''}</span>
|
|
107
|
+
<span style="flex:1"></span>
|
|
108
|
+
<button onclick="toggleOffline()" style="background:none;border:1px solid var(--border);color:var(--text-dim);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">${showOffline ? 'Hide offline' : 'Show offline'}</button>
|
|
109
|
+
${offlineCount > 0 ? '<button onclick="cleanOffline()" style="background:none;border:1px solid var(--red);color:var(--red);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">Clean offline</button>' : ''}
|
|
110
|
+
<button onclick="loadSessions()" style="background:none;border:1px solid var(--border);color:var(--text-dim);font-family:var(--font);font-size:11px;padding:3px 8px;border-radius:4px;cursor:pointer">↻</button>
|
|
111
|
+
</div>`;
|
|
112
|
+
|
|
113
|
+
if (filtered.length === 0) {
|
|
114
|
+
html += '<div style="padding:20px 12px;color:var(--text-dim);text-align:center">' +
|
|
115
|
+
(sessions.length === 0 ? 'No Squad RC sessions found.' : 'No online sessions. Tap "Show offline" to see stale ones.') +
|
|
116
|
+
'</div>';
|
|
117
|
+
} else {
|
|
118
|
+
html += filtered.map(s => `
|
|
119
|
+
<div class="session-card" ${s.online ? 'data-session-url="' + escapeHtml(s.url) + '"' : ''}>
|
|
120
|
+
<span class="status-dot ${s.online ? 'online' : 'offline'}"></span>
|
|
121
|
+
<div class="info">
|
|
122
|
+
<div class="repo">📦 ${escapeHtml(s.repo)}</div>
|
|
123
|
+
<div class="branch">🌿 ${escapeHtml(s.branch)}</div>
|
|
124
|
+
<div class="machine">💻 ${escapeHtml(s.machine)}</div>
|
|
125
|
+
</div>
|
|
126
|
+
${s.online ? '<span class="arrow">→</span>' :
|
|
127
|
+
'<button data-delete-id="' + escapeHtml(s.id) + '" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px" title="Remove">✕</button>'}
|
|
128
|
+
</div>
|
|
129
|
+
`).join('');
|
|
130
|
+
}
|
|
131
|
+
dashboard.innerHTML = html;
|
|
132
|
+
// #16: XSS fix — use event delegation instead of inline onclick
|
|
133
|
+
dashboard.querySelectorAll('.session-card[data-session-url]').forEach(function(card) {
|
|
134
|
+
card.addEventListener('click', function() { openSession(card.dataset.sessionUrl); });
|
|
135
|
+
});
|
|
136
|
+
dashboard.querySelectorAll('[data-delete-id]').forEach(function(btn) {
|
|
137
|
+
btn.addEventListener('click', function(e) { e.stopPropagation(); deleteSession(btn.dataset.deleteId); });
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
window.openSession = (url) => {
|
|
142
|
+
window.location.href = url;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
window.toggleOffline = () => {
|
|
146
|
+
showOffline = !showOffline;
|
|
147
|
+
loadSessions();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
window.cleanOffline = async () => {
|
|
151
|
+
const resp = await fetch('/api/sessions');
|
|
152
|
+
const data = await resp.json();
|
|
153
|
+
const offline = (data.sessions || []).filter(s => !s.online);
|
|
154
|
+
for (const s of offline) {
|
|
155
|
+
await fetch('/api/sessions/' + s.id, { method: 'DELETE' });
|
|
156
|
+
}
|
|
157
|
+
loadSessions();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
window.deleteSession = async (id) => {
|
|
161
|
+
await fetch('/api/sessions/' + id, { method: 'DELETE' });
|
|
162
|
+
loadSessions();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
window.toggleView = () => {
|
|
166
|
+
if (currentView === 'terminal') {
|
|
167
|
+
currentView = 'dashboard';
|
|
168
|
+
terminal.classList.add('hidden');
|
|
169
|
+
termContainer.classList.add('hidden');
|
|
170
|
+
$('#input-area').classList.add('hidden');
|
|
171
|
+
dashboard.classList.remove('hidden');
|
|
172
|
+
$('#btn-sessions').textContent = 'Terminal';
|
|
173
|
+
loadSessions();
|
|
174
|
+
} else {
|
|
175
|
+
currentView = 'terminal';
|
|
176
|
+
dashboard.classList.add('hidden');
|
|
177
|
+
$('#input-area').classList.remove('hidden');
|
|
178
|
+
if (ptyMode) {
|
|
179
|
+
termContainer.classList.remove('hidden');
|
|
180
|
+
$('#input-form').classList.add('hidden');
|
|
181
|
+
if (fitAddon) fitAddon.fit();
|
|
182
|
+
if (xterm) xterm.focus();
|
|
183
|
+
} else {
|
|
184
|
+
terminal.classList.remove('hidden');
|
|
185
|
+
}
|
|
186
|
+
$('#btn-sessions').textContent = 'Sessions';
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// ─── Terminal Output ─────────────────────────────────────
|
|
191
|
+
function writeSys(text) {
|
|
192
|
+
const div = document.createElement('div');
|
|
193
|
+
div.className = 'sys';
|
|
194
|
+
div.textContent = text;
|
|
195
|
+
terminal.appendChild(div);
|
|
196
|
+
if (!replaying) scrollToBottom();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── WebSocket ───────────────────────────────────────────
|
|
200
|
+
async function connect() {
|
|
201
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
202
|
+
if (!tokenParam) { setStatus('offline', 'No credentials'); return; }
|
|
203
|
+
|
|
204
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
205
|
+
|
|
206
|
+
// F-02: Try ticket-based auth first
|
|
207
|
+
try {
|
|
208
|
+
const resp = await fetch('/api/auth/ticket', {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { 'Authorization': 'Bearer ' + tokenParam }
|
|
211
|
+
});
|
|
212
|
+
if (resp.ok) {
|
|
213
|
+
const { ticket } = await resp.json();
|
|
214
|
+
ws = new WebSocket(`${proto}//${location.host}?ticket=${encodeURIComponent(ticket)}`);
|
|
215
|
+
} else {
|
|
216
|
+
// Fallback to token-in-URL (backward compat)
|
|
217
|
+
ws = new WebSocket(`${proto}//${location.host}?token=${encodeURIComponent(tokenParam)}`);
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// Fallback to token-in-URL
|
|
221
|
+
ws = new WebSocket(`${proto}//${location.host}?token=${encodeURIComponent(tokenParam)}`);
|
|
222
|
+
}
|
|
223
|
+
setStatus('connecting', 'Connecting...');
|
|
224
|
+
|
|
225
|
+
ws.onopen = () => {
|
|
226
|
+
connected = true;
|
|
227
|
+
reconnectDelay = 1000;
|
|
228
|
+
setStatus('online', 'Connected — waiting for terminal...');
|
|
229
|
+
};
|
|
230
|
+
ws.onclose = () => {
|
|
231
|
+
connected = false;
|
|
232
|
+
setStatus('offline', 'Disconnected');
|
|
233
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
234
|
+
setTimeout(connect, reconnectDelay);
|
|
235
|
+
};
|
|
236
|
+
ws.onerror = () => setStatus('offline', 'Error');
|
|
237
|
+
ws.onmessage = (e) => {
|
|
238
|
+
try {
|
|
239
|
+
const msg = JSON.parse(e.data);
|
|
240
|
+
handleMessage(msg);
|
|
241
|
+
} catch {}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Message Handler ─────────────────────────────────────
|
|
246
|
+
function handleMessage(msg) {
|
|
247
|
+
// Replay events from bridge recording
|
|
248
|
+
if (msg.type === '_replay') {
|
|
249
|
+
replaying = true;
|
|
250
|
+
try { handleMessage(JSON.parse(msg.data)); } catch {}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (msg.type === '_replay_done') {
|
|
254
|
+
replaying = false;
|
|
255
|
+
scrollToBottom();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// PTY data — raw terminal output → xterm.js
|
|
260
|
+
if (msg.type === 'pty') {
|
|
261
|
+
if (!ptyMode) {
|
|
262
|
+
ptyMode = true;
|
|
263
|
+
setStatus('online', 'PTY Mirror');
|
|
264
|
+
terminal.classList.add('hidden');
|
|
265
|
+
// Hide text input form but keep key bar visible
|
|
266
|
+
$('#input-form').classList.add('hidden');
|
|
267
|
+
termContainer.classList.remove('hidden');
|
|
268
|
+
initXterm();
|
|
269
|
+
}
|
|
270
|
+
xterm.write(msg.data);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Clear screen detection
|
|
275
|
+
if (msg.type === 'clear') {
|
|
276
|
+
if (xterm) xterm.clear();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Mobile Key Bar ───────────────────────────────────────
|
|
282
|
+
let ptyMode = false;
|
|
283
|
+
|
|
284
|
+
window.sendKey = (key) => {
|
|
285
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
286
|
+
ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
287
|
+
}
|
|
288
|
+
if (xterm) xterm.focus();
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Event delegation for key-bar buttons (no inline onclick)
|
|
292
|
+
var keyBar = document.getElementById('key-bar');
|
|
293
|
+
if (keyBar) {
|
|
294
|
+
var keyMap = {
|
|
295
|
+
'\\x1b[A': '\x1b[A', '\\x1b[B': '\x1b[B', '\\x1b[C': '\x1b[C', '\\x1b[D': '\x1b[D',
|
|
296
|
+
'\\t': '\t', '\\r': '\r', '\\x1b': '\x1b', '\\x03': '\x03', ' ': ' ', '\\x7f': '\x7f',
|
|
297
|
+
};
|
|
298
|
+
keyBar.addEventListener('click', function(e) {
|
|
299
|
+
var btn = e.target;
|
|
300
|
+
if (btn && btn.tagName === 'BUTTON' && btn.dataset.key) {
|
|
301
|
+
var key = keyMap[btn.dataset.key] || btn.dataset.key;
|
|
302
|
+
window.sendKey(key);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Event listener for btn-sessions (no inline onclick)
|
|
308
|
+
var btnSessions = document.getElementById('btn-sessions');
|
|
309
|
+
if (btnSessions) {
|
|
310
|
+
btnSessions.addEventListener('click', function() { window.toggleView(); });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Form submit — in PTY mode, just focus xterm
|
|
314
|
+
formEl.addEventListener('submit', function(e) {
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
if (xterm) xterm.focus();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
320
|
+
function setStatus(state, text) {
|
|
321
|
+
statusEl.className = state;
|
|
322
|
+
statusText.textContent = text;
|
|
323
|
+
}
|
|
324
|
+
function scrollToBottom() {
|
|
325
|
+
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
326
|
+
}
|
|
327
|
+
function escapeHtml(s) {
|
|
328
|
+
var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''').replace(/"/g, '"');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Start ───────────────────────────────────────────────
|
|
332
|
+
writeSys('Squad Remote Control');
|
|
333
|
+
connect();
|
|
334
|
+
})();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
6
|
+
<meta name="theme-color" content="#0d1117">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
+
<title>Squad RC</title>
|
|
10
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" integrity="sha384-tStR1zLfWgsiXCF3IgfB3lBa8KmBe/lG287CL9WCeKgQYcp1bjb4/+mwN6oti4Co" crossorigin="anonymous">
|
|
11
|
+
<link rel="stylesheet" href="/styles.css">
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app">
|
|
15
|
+
<header id="header">
|
|
16
|
+
<span id="status-indicator">●</span>
|
|
17
|
+
<span id="status-text">Connecting...</span>
|
|
18
|
+
<span style="flex:1"></span>
|
|
19
|
+
<button id="btn-sessions" style="background:none;border:none;color:var(--text-dim);font-family:var(--font);font-size:12px;cursor:pointer">Sessions</button>
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<!-- Dashboard view -->
|
|
23
|
+
<div id="dashboard" class="hidden">
|
|
24
|
+
<div style="padding:12px;color:var(--text-dim);font-size:12px">Loading sessions...</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Terminal view (xterm.js) -->
|
|
28
|
+
<div id="terminal-container"></div>
|
|
29
|
+
|
|
30
|
+
<!-- Legacy terminal (ACP mode fallback) -->
|
|
31
|
+
<main id="terminal" class="hidden"></main>
|
|
32
|
+
|
|
33
|
+
<footer id="input-area">
|
|
34
|
+
<div id="key-bar">
|
|
35
|
+
<button data-key="\x1b[A">↑</button>
|
|
36
|
+
<button data-key="\x1b[B">↓</button>
|
|
37
|
+
<button data-key="\x1b[C">→</button>
|
|
38
|
+
<button data-key="\x1b[D">←</button>
|
|
39
|
+
<button data-key="\t">Tab</button>
|
|
40
|
+
<button data-key="\r">Enter</button>
|
|
41
|
+
<button data-key="\x1b">Esc</button>
|
|
42
|
+
<button data-key="\x03">Ctrl+C</button>
|
|
43
|
+
<button data-key=" ">Space</button>
|
|
44
|
+
<button data-key="\x7f">⌫</button>
|
|
45
|
+
</div>
|
|
46
|
+
<form id="input-form">
|
|
47
|
+
<span class="prompt">></span>
|
|
48
|
+
<input type="text" id="input" placeholder="Send a message..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
|
49
|
+
</form>
|
|
50
|
+
</footer>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="permission-overlay" class="hidden"></div>
|
|
53
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js" integrity="sha384-J4qzUjBl1FxyLsl/kQPQIOeINsmp17OHYXDOMpMxlKX53ZfYsL+aWHpgArvOuof9" crossorigin="anonymous"></script>
|
|
54
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js" integrity="sha384-XGqKrV8Jrukp1NITJbOEHwg01tNkuXr6uB6YEj69ebpYU3v7FvoGgEg23C1Gcehk" crossorigin="anonymous"></script>
|
|
55
|
+
<script src="/app.js"></script>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0d1117;
|
|
3
|
+
--bg-tool: #161b22;
|
|
4
|
+
--text: #c9d1d9;
|
|
5
|
+
--text-dim: #6e7681;
|
|
6
|
+
--text-bright: #f0f6fc;
|
|
7
|
+
--green: #3fb950;
|
|
8
|
+
--red: #f85149;
|
|
9
|
+
--yellow: #d29922;
|
|
10
|
+
--blue: #58a6ff;
|
|
11
|
+
--purple: #bc8cff;
|
|
12
|
+
--cyan: #39c5cf;
|
|
13
|
+
--border: #30363d;
|
|
14
|
+
--font: 'Cascadia Code', 'SF Mono', 'Fira Code', 'Menlo', 'Consolas', monospace;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
font-family: var(--font);
|
|
21
|
+
font-size: 13px;
|
|
22
|
+
background: var(--bg);
|
|
23
|
+
color: var(--text);
|
|
24
|
+
height: 100dvh;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
-webkit-font-smoothing: antialiased;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#app {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
height: 100dvh;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Header — minimal status bar */
|
|
36
|
+
header {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 8px;
|
|
40
|
+
padding: 6px 12px;
|
|
41
|
+
background: var(--bg-tool);
|
|
42
|
+
border-bottom: 1px solid var(--border);
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
font-size: 12px;
|
|
45
|
+
}
|
|
46
|
+
#status-indicator { font-size: 10px; }
|
|
47
|
+
#status-indicator.online { color: var(--green); }
|
|
48
|
+
#status-indicator.offline { color: var(--red); }
|
|
49
|
+
#status-indicator.connecting { color: var(--yellow); }
|
|
50
|
+
#status-text { color: var(--text-dim); }
|
|
51
|
+
|
|
52
|
+
/* Terminal area (legacy) */
|
|
53
|
+
#terminal {
|
|
54
|
+
flex: 1;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
padding: 8px 12px;
|
|
57
|
+
white-space: pre-wrap;
|
|
58
|
+
word-wrap: break-word;
|
|
59
|
+
line-height: 1.5;
|
|
60
|
+
scroll-behavior: smooth;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* xterm.js container */
|
|
64
|
+
#terminal-container {
|
|
65
|
+
flex: 1;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
}
|
|
68
|
+
#terminal-container .xterm {
|
|
69
|
+
height: 100%;
|
|
70
|
+
padding: 4px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* System messages */
|
|
74
|
+
.sys { color: var(--text-dim); font-style: italic; }
|
|
75
|
+
|
|
76
|
+
/* User input echo */
|
|
77
|
+
.user-input { color: var(--blue); }
|
|
78
|
+
.user-input::before { content: '❯ '; color: var(--green); }
|
|
79
|
+
|
|
80
|
+
/* Agent text */
|
|
81
|
+
.agent-text { color: var(--text); }
|
|
82
|
+
|
|
83
|
+
/* Streaming cursor */
|
|
84
|
+
.cursor {
|
|
85
|
+
display: inline-block;
|
|
86
|
+
width: 7px; height: 14px;
|
|
87
|
+
background: var(--text);
|
|
88
|
+
animation: blink 0.7s infinite;
|
|
89
|
+
vertical-align: text-bottom;
|
|
90
|
+
margin-left: 1px;
|
|
91
|
+
}
|
|
92
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
93
|
+
|
|
94
|
+
/* Tool calls */
|
|
95
|
+
.tool-call {
|
|
96
|
+
margin: 4px 0;
|
|
97
|
+
border-left: 2px solid var(--blue);
|
|
98
|
+
padding: 2px 0 2px 8px;
|
|
99
|
+
color: var(--text-dim);
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
}
|
|
102
|
+
.tool-call.completed { border-left-color: var(--green); }
|
|
103
|
+
.tool-call.failed { border-left-color: var(--red); }
|
|
104
|
+
.tool-call .tool-icon { margin-right: 4px; }
|
|
105
|
+
.tool-call .tool-name { color: var(--cyan); }
|
|
106
|
+
.tool-call .tool-status { margin-left: 8px; }
|
|
107
|
+
.tool-call .tool-status.completed { color: var(--green); }
|
|
108
|
+
.tool-call .tool-status.failed { color: var(--red); }
|
|
109
|
+
.tool-call .tool-status.in_progress { color: var(--yellow); }
|
|
110
|
+
|
|
111
|
+
/* Tool call content (expandable) */
|
|
112
|
+
.tool-body { display: none; margin-top: 4px; }
|
|
113
|
+
.tool-call.expanded .tool-body { display: block; }
|
|
114
|
+
|
|
115
|
+
/* Diff blocks */
|
|
116
|
+
.diff { margin: 4px 0; font-size: 12px; }
|
|
117
|
+
.diff-header { color: var(--text-dim); }
|
|
118
|
+
.diff-add { color: var(--green); }
|
|
119
|
+
.diff-add::before { content: '+ '; }
|
|
120
|
+
.diff-del { color: var(--red); }
|
|
121
|
+
.diff-del::before { content: '- '; }
|
|
122
|
+
|
|
123
|
+
/* Code blocks */
|
|
124
|
+
.code-block {
|
|
125
|
+
background: var(--bg-tool);
|
|
126
|
+
border: 1px solid var(--border);
|
|
127
|
+
border-radius: 4px;
|
|
128
|
+
padding: 6px 8px;
|
|
129
|
+
margin: 4px 0;
|
|
130
|
+
overflow-x: auto;
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
}
|
|
133
|
+
.code-header { color: var(--text-dim); font-size: 11px; margin-bottom: 2px; }
|
|
134
|
+
|
|
135
|
+
/* Permission dialog */
|
|
136
|
+
#permission-overlay {
|
|
137
|
+
position: fixed;
|
|
138
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
139
|
+
background: rgba(0,0,0,0.7);
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: flex-end;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
padding-bottom: 60px;
|
|
144
|
+
z-index: 100;
|
|
145
|
+
}
|
|
146
|
+
#permission-overlay.hidden { display: none; }
|
|
147
|
+
.perm-dialog {
|
|
148
|
+
background: var(--bg-tool);
|
|
149
|
+
border: 1px solid var(--yellow);
|
|
150
|
+
border-radius: 8px;
|
|
151
|
+
padding: 12px;
|
|
152
|
+
width: calc(100% - 24px);
|
|
153
|
+
max-width: 400px;
|
|
154
|
+
max-height: 40vh;
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
}
|
|
158
|
+
.perm-dialog h3 { color: var(--yellow); font-size: 14px; margin-bottom: 6px; flex-shrink: 0; }
|
|
159
|
+
.perm-dialog p { font-size: 12px; color: var(--text); margin-bottom: 10px; overflow-y: auto; flex: 1; min-height: 0; }
|
|
160
|
+
.perm-actions { display: flex; gap: 8px; justify-content: flex-end; flex-shrink: 0; padding-top: 4px; }
|
|
161
|
+
.perm-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|
162
|
+
.perm-actions button {
|
|
163
|
+
padding: 6px 16px; border: none; border-radius: 4px;
|
|
164
|
+
cursor: pointer; font-size: 13px; font-family: var(--font);
|
|
165
|
+
}
|
|
166
|
+
.btn-approve { background: var(--green); color: #000; }
|
|
167
|
+
.btn-deny { background: var(--red); color: #fff; }
|
|
168
|
+
|
|
169
|
+
/* Input area */
|
|
170
|
+
#input-area {
|
|
171
|
+
padding: 4px 8px 6px;
|
|
172
|
+
background: var(--bg-tool);
|
|
173
|
+
border-top: 1px solid var(--border);
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
}
|
|
176
|
+
#key-bar {
|
|
177
|
+
display: flex;
|
|
178
|
+
gap: 4px;
|
|
179
|
+
padding: 4px 0;
|
|
180
|
+
overflow-x: auto;
|
|
181
|
+
-webkit-overflow-scrolling: touch;
|
|
182
|
+
}
|
|
183
|
+
#key-bar button {
|
|
184
|
+
background: var(--bg);
|
|
185
|
+
border: 1px solid var(--border);
|
|
186
|
+
color: var(--text);
|
|
187
|
+
font-family: var(--font);
|
|
188
|
+
font-size: 13px;
|
|
189
|
+
padding: 6px 10px;
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
cursor: pointer;
|
|
192
|
+
flex-shrink: 0;
|
|
193
|
+
min-width: 36px;
|
|
194
|
+
-webkit-tap-highlight-color: transparent;
|
|
195
|
+
}
|
|
196
|
+
#key-bar button:active { background: var(--blue); color: #000; }
|
|
197
|
+
#input-form {
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
gap: 4px;
|
|
201
|
+
}
|
|
202
|
+
.prompt { color: var(--green); font-weight: bold; }
|
|
203
|
+
#input {
|
|
204
|
+
flex: 1;
|
|
205
|
+
background: transparent;
|
|
206
|
+
border: none;
|
|
207
|
+
color: var(--text-bright);
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
font-family: var(--font);
|
|
210
|
+
outline: none;
|
|
211
|
+
caret-color: var(--green);
|
|
212
|
+
}
|
|
213
|
+
#input::placeholder { color: var(--text-dim); }
|
|
214
|
+
|
|
215
|
+
.hidden { display: none !important; }
|
|
216
|
+
|
|
217
|
+
/* Dashboard */
|
|
218
|
+
#dashboard {
|
|
219
|
+
flex: 1;
|
|
220
|
+
overflow-y: auto;
|
|
221
|
+
padding: 8px;
|
|
222
|
+
}
|
|
223
|
+
.session-card {
|
|
224
|
+
background: var(--bg-tool);
|
|
225
|
+
border: 1px solid var(--border);
|
|
226
|
+
border-radius: 6px;
|
|
227
|
+
padding: 10px 12px;
|
|
228
|
+
margin-bottom: 6px;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
gap: 10px;
|
|
233
|
+
}
|
|
234
|
+
.session-card:hover { border-color: var(--blue); }
|
|
235
|
+
.session-card .status-dot {
|
|
236
|
+
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
237
|
+
}
|
|
238
|
+
.session-card .status-dot.online { background: var(--green); }
|
|
239
|
+
.session-card .status-dot.offline { background: var(--text-dim); }
|
|
240
|
+
.session-card .info { flex: 1; min-width: 0; }
|
|
241
|
+
.session-card .repo { color: var(--blue); font-weight: bold; font-size: 13px; }
|
|
242
|
+
.session-card .branch { color: var(--text-dim); font-size: 11px; }
|
|
243
|
+
.session-card .machine { color: var(--text-dim); font-size: 11px; }
|
|
244
|
+
.session-card .arrow { color: var(--text-dim); }
|
|
245
|
+
|
|
246
|
+
/* Scrollbar */
|
|
247
|
+
::-webkit-scrollbar { width: 6px; }
|
|
248
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
249
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bradygaster/squad-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.24",
|
|
4
4
|
"description": "Squad CLI — Command-line interface for the Squad multi-agent runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -124,6 +124,10 @@
|
|
|
124
124
|
"types": "./dist/cli/commands/rc-tunnel.d.ts",
|
|
125
125
|
"import": "./dist/cli/commands/rc-tunnel.js"
|
|
126
126
|
},
|
|
127
|
+
"./commands/rc": {
|
|
128
|
+
"types": "./dist/cli/commands/rc.d.ts",
|
|
129
|
+
"import": "./dist/cli/commands/rc.js"
|
|
130
|
+
},
|
|
127
131
|
"./commands/extract": {
|
|
128
132
|
"types": "./dist/cli/commands/extract.d.ts",
|
|
129
133
|
"import": "./dist/cli/commands/extract.js"
|
|
@@ -140,11 +144,14 @@
|
|
|
140
144
|
"files": [
|
|
141
145
|
"dist",
|
|
142
146
|
"templates",
|
|
147
|
+
"scripts",
|
|
143
148
|
"README.md"
|
|
144
149
|
],
|
|
145
150
|
"scripts": {
|
|
151
|
+
"postinstall": "node scripts/patch-esm-imports.mjs",
|
|
146
152
|
"prepublishOnly": "npm run build",
|
|
147
|
-
"build": "tsc -p tsconfig.json"
|
|
153
|
+
"build": "tsc -p tsconfig.json && npm run postbuild",
|
|
154
|
+
"postbuild": "node -e \"require('fs').cpSync('src/remote-ui', 'dist/remote-ui', {recursive: true})\""
|
|
148
155
|
},
|
|
149
156
|
"engines": {
|
|
150
157
|
"node": ">=20"
|