thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.4
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/Readme.md +43 -10
- package/dist/native/brand/favicon-64.png +0 -0
- package/dist/native/brand/tnc.ico +0 -0
- package/dist/native/tnc-tray/build-macos.sh +59 -0
- package/dist/native/tnc-tray/main.swift +204 -0
- package/dist/native/tnc-tray/tray.ps1 +137 -0
- package/dist/src/commands/dock/containers.js +1 -1
- package/dist/src/commands/dock/dock.js +1 -1
- package/dist/src/dock/api/routes.js +1 -1
- package/dist/src/dock/api/server.js +1 -1
- package/dist/src/dock/api/websocket.js +7 -0
- package/dist/src/dock/api/wire.js +1 -1
- package/dist/src/dock/build/builder.js +1 -1
- package/dist/src/dock/build/buildkit.js +1 -1
- package/dist/src/dock/build/cache.js +7 -0
- package/dist/src/dock/build/sshagent.js +7 -0
- package/dist/src/dock/cli/api.js +1 -1
- package/dist/src/dock/cli/commands.js +1 -1
- package/dist/src/dock/cli/compose.js +1 -1
- package/dist/src/dock/cli/trust.js +7 -0
- package/dist/src/dock/compat/cases.js +1 -1
- package/dist/src/dock/config.js +1 -1
- package/dist/src/dock/daemon-main.js +1 -1
- package/dist/src/dock/daemon.js +1 -1
- package/dist/src/dock/engine.js +1 -1
- package/dist/src/dock/image/auth.js +1 -1
- package/dist/src/dock/image/credhelpers.js +7 -0
- package/dist/src/dock/image/keychain.js +7 -0
- package/dist/src/dock/image/registry.js +1 -1
- package/dist/src/dock/image/scan.js +7 -0
- package/dist/src/dock/image/sign.js +7 -0
- package/dist/src/dock/linux/host.js +1 -1
- package/dist/src/dock/paths.js +1 -1
- package/dist/src/dock/runtime/cdi.js +7 -0
- package/dist/src/dock/runtime/fsops.js +1 -1
- package/dist/src/dock/runtime/network.js +1 -1
- package/dist/src/dock/runtime/overlaydns.js +7 -0
- package/dist/src/dock/runtime/ports.js +1 -1
- package/dist/src/dock/runtime/rootfs.js +1 -1
- package/dist/src/dock/runtime/spec.js +1 -1
- package/dist/src/dock/runtime/stats.js +1 -1
- package/dist/src/dock/runtime/userns.js +7 -0
- package/dist/src/dock/runtime/volumes.js +1 -1
- package/dist/src/dock/setup/autostart.js +1 -1
- package/dist/src/dock/setup/diagnose.js +7 -0
- package/dist/src/dock/setup/index.js +1 -1
- package/dist/src/dock/tray/agent.js +7 -0
- package/dist/src/dock/tray/install.js +7 -0
- package/dist/src/dock/ui/page.js +232 -16
- package/dist/src/dock/ui/server.js +1 -1
- package/package.json +1 -1
- package/dist/src/dock/setup/app.js +0 -7
package/dist/src/dock/ui/page.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const PAGE=String.raw`<!doctype html>
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path");function brandImage(e){try{return`data:image/png;base64,${fs.readFileSync(path.join(__dirname,"..","..","..","native","brand",e)).toString("base64")}`}catch(e){return""}}const FAVICON=brandImage("favicon-64.png"),LOGO=brandImage("favicon-64.png"),PAGE=String.raw`<!doctype html>
|
|
8
8
|
<html lang="en">
|
|
9
9
|
<head>
|
|
10
10
|
<meta charset="utf-8">
|
|
11
11
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
12
12
|
<title>dock dashboard</title>
|
|
13
|
+
${FAVICON?`<link rel="icon" type="image/png" href="${FAVICON}">`:""}
|
|
13
14
|
<style>
|
|
14
15
|
:root {
|
|
15
16
|
--bg: #f6f7f9; --panel: #ffffff; --text: #18202b; --muted: #5d6878; --line: #e3e7ed;
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
30
31
|
header { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 2; }
|
|
31
32
|
header h1 { font-size: 16px; margin: 0; letter-spacing: .2px; }
|
|
33
|
+
header .logo { width: 24px; height: 24px; border-radius: 6px; }
|
|
32
34
|
header .meta { color: var(--muted); font-size: 12px; }
|
|
33
35
|
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); display: inline-block; }
|
|
34
36
|
.dot.on { background: var(--ok); }
|
|
@@ -68,10 +70,28 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
68
70
|
#toast { position: fixed; right: 16px; bottom: 16px; max-width: 420px; display: grid; gap: 8px; z-index: 5; }
|
|
69
71
|
#toast div { background: var(--panel); border: 1px solid var(--bad); color: var(--text); padding: 10px 12px; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,.15); }
|
|
70
72
|
#locked { padding: 60px 20px; text-align: center; color: var(--muted); }
|
|
73
|
+
#toast div.ok { border-color: var(--ok); }
|
|
74
|
+
input.search, .form input[type=text], .form input[type=number], .form textarea, .form select { font: inherit; font-size: 13px; color: var(--text); background: var(--bg); border: 1px solid var(--line); border-radius: 7px; padding: 5px 8px; }
|
|
75
|
+
input.search { min-width: 220px; }
|
|
76
|
+
tr.group td { background: var(--chip); font-weight: 600; cursor: default; }
|
|
77
|
+
.form { display: grid; gap: 18px; max-width: 760px; }
|
|
78
|
+
.form fieldset { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0; display: grid; gap: 10px; }
|
|
79
|
+
.form legend { font-weight: 600; padding: 0 6px; }
|
|
80
|
+
.form .field { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 10px; align-items: center; }
|
|
81
|
+
.form .field > span { color: var(--muted); font-size: 13px; }
|
|
82
|
+
.form textarea { min-height: 54px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
|
|
83
|
+
.form .hint { color: var(--muted); font-size: 12px; grid-column: 2; margin-top: -6px; }
|
|
84
|
+
@media (max-width: 640px) { .form .field { grid-template-columns: 1fr; } .form .hint { grid-column: 1; } }
|
|
85
|
+
pre.term { background: var(--code); color: var(--codetext); margin: 0; padding: 12px; border-radius: 8px; height: 360px; overflow: auto; white-space: pre-wrap; word-break: break-all; font: 12.5px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; outline: none; }
|
|
86
|
+
pre.term:focus { box-shadow: 0 0 0 2px var(--accent); }
|
|
87
|
+
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
|
|
88
|
+
.seg button { border: 0; background: var(--panel); color: var(--muted); font: inherit; font-size: 12.5px; padding: 4px 10px; cursor: pointer; }
|
|
89
|
+
.seg button.on { background: var(--chip); color: var(--text); }
|
|
71
90
|
</style>
|
|
72
91
|
</head>
|
|
73
92
|
<body>
|
|
74
93
|
<header>
|
|
94
|
+
${LOGO?`<img class="logo" src="${LOGO}" alt="ThinkNCollab" width="24" height="24">`:""}
|
|
75
95
|
<span class="dot" id="dot"></span>
|
|
76
96
|
<h1>dock</h1>
|
|
77
97
|
<span class="meta" id="meta">connecting…</span>
|
|
@@ -86,6 +106,7 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
86
106
|
(function () {
|
|
87
107
|
'use strict';
|
|
88
108
|
var token = (location.hash.match(/t=([0-9a-f]+)/) || [])[1] || sessionStorage.getItem('tnc-ui-token');
|
|
109
|
+
var startTab = (location.hash.match(/tab=([a-z]+)/) || [])[1];
|
|
89
110
|
if (token) { sessionStorage.setItem('tnc-ui-token', token); history.replaceState(null, '', location.pathname); }
|
|
90
111
|
|
|
91
112
|
var root = document.documentElement;
|
|
@@ -108,13 +129,15 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
108
129
|
(kids || []).forEach(function (c) { if (c != null) n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); });
|
|
109
130
|
return n;
|
|
110
131
|
}
|
|
111
|
-
function toast(msg) {
|
|
112
|
-
var box = el('div', { text: msg });
|
|
132
|
+
function toast(msg, ok) {
|
|
133
|
+
var box = el('div', { text: msg, class: ok ? 'ok' : '' });
|
|
113
134
|
document.getElementById('toast').appendChild(box);
|
|
114
135
|
setTimeout(function () { box.remove(); }, 6000);
|
|
115
136
|
}
|
|
116
|
-
function api(method, path) {
|
|
117
|
-
|
|
137
|
+
function api(method, path, body, prefix) {
|
|
138
|
+
var headers = { 'x-tnc-ui': token || '' };
|
|
139
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
140
|
+
return fetch((prefix || '/api') + path, { method: method, headers: headers, body: body === undefined ? undefined : JSON.stringify(body) }).then(function (r) {
|
|
118
141
|
if (r.status === 204 || r.status === 304) return null;
|
|
119
142
|
return r.text().then(function (t) {
|
|
120
143
|
var body; try { body = t ? JSON.parse(t) : null; } catch (e) { body = t; }
|
|
@@ -132,8 +155,11 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
132
155
|
var ago = function (s) { var d = Date.now() / 1000 - s; if (d < 60) return 'just now'; if (d < 3600) return Math.floor(d / 60) + ' min ago'; if (d < 86400) return Math.floor(d / 3600) + ' h ago'; return Math.floor(d / 86400) + ' d ago'; };
|
|
133
156
|
var short = function (id) { return String(id || '').replace(/^sha256:/, '').slice(0, 12); };
|
|
134
157
|
|
|
135
|
-
var TABS = ['Containers', 'Images', 'Volumes', 'Networks', 'System'];
|
|
136
|
-
var state = { tab: 'Containers', all: true, selected: null };
|
|
158
|
+
var TABS = ['Containers', 'Images', 'Volumes', 'Networks', 'System', 'Settings', 'Troubleshoot'];
|
|
159
|
+
var state = { tab: 'Containers', all: true, selected: null, filter: '', pane: 'logs' };
|
|
160
|
+
TABS.forEach(function (t) { if (startTab && t.toLowerCase() === startTab) state.tab = t; });
|
|
161
|
+
var ui = function (method, path, body) { return api(method, path, body, '/ui'); };
|
|
162
|
+
var PROJECT = 'tnc.compose.project';
|
|
137
163
|
var view = document.getElementById('view');
|
|
138
164
|
|
|
139
165
|
function renderTabs() {
|
|
@@ -153,12 +179,39 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
153
179
|
/* ── containers ── */
|
|
154
180
|
function containers() {
|
|
155
181
|
return api('GET', '/containers/json' + (state.all ? '?all=1' : '')).then(function (list) {
|
|
182
|
+
var q = state.filter.toLowerCase();
|
|
183
|
+
if (q) list = list.filter(function (c) { return ((c.Names || []).join(' ') + ' ' + c.Image + ' ' + ((c.Labels || {})[PROJECT] || '')).toLowerCase().indexOf(q) !== -1; });
|
|
184
|
+
var search = el('input', { class: 'search', type: 'search', placeholder: 'Search name, image or app', value: state.filter, oninput: function (e) { state.filter = e.target.value; refresh(); } });
|
|
156
185
|
var bar = el('div', { class: 'toolbar' }, [
|
|
186
|
+
search,
|
|
157
187
|
el('label', null, [el('input', { type: 'checkbox', checked: state.all, onchange: function (e) { state.all = e.target.checked; refresh(); } }), 'show stopped']),
|
|
158
188
|
el('span', { class: 'spacer' }),
|
|
159
189
|
btn('Remove stopped', function () { act('POST', '/containers/prune', 'Remove every stopped container?'); }),
|
|
160
190
|
]);
|
|
161
|
-
|
|
191
|
+
// compose apps first (grouped by project), then single containers
|
|
192
|
+
list.sort(function (a, b) {
|
|
193
|
+
var pa = (a.Labels || {})[PROJECT] || '\uffff', pb = (b.Labels || {})[PROJECT] || '\uffff';
|
|
194
|
+
return pa < pb ? -1 : pa > pb ? 1 : (a.Names[0] < b.Names[0] ? -1 : 1);
|
|
195
|
+
});
|
|
196
|
+
var rows = [];
|
|
197
|
+
var lastGroup = null;
|
|
198
|
+
list.forEach(function (c) {
|
|
199
|
+
var project = (c.Labels || {})[PROJECT] || null;
|
|
200
|
+
if (project !== lastGroup) {
|
|
201
|
+
lastGroup = project;
|
|
202
|
+
var members = list.filter(function (x) { return ((x.Labels || {})[PROJECT] || null) === project; });
|
|
203
|
+
var up = members.filter(function (x) { return x.State === 'running'; });
|
|
204
|
+
rows.push(el('tr', { class: 'group' }, [
|
|
205
|
+
el('td', { colspan: '5', text: project ? project + ' · ' + up.length + '/' + members.length + ' running' : 'Other containers' }),
|
|
206
|
+
el('td', null, [project ? el('div', { class: 'actions' }, [
|
|
207
|
+
btn('Start all', function () { Promise.all(members.filter(function (x) { return x.State !== 'running'; }).map(function (x) { return api('POST', '/containers/' + x.Id + '/start'); })).then(refresh).catch(function (e) { toast(e.message); }); }, { disabled: up.length === members.length }),
|
|
208
|
+
btn('Stop all', function () { Promise.all(up.map(function (x) { return api('POST', '/containers/' + x.Id + '/stop'); })).then(refresh).catch(function (e) { toast(e.message); }); }, { disabled: !up.length }),
|
|
209
|
+
]) : null]),
|
|
210
|
+
]));
|
|
211
|
+
}
|
|
212
|
+
rows.push(containerRow(c));
|
|
213
|
+
});
|
|
214
|
+
function containerRow(c) {
|
|
162
215
|
var name = (c.Names && c.Names[0] || '').replace(/^\//, '');
|
|
163
216
|
var st = c.State;
|
|
164
217
|
var ports = (c.Ports || []).filter(function (p) { return p.PublicPort; }).map(function (p) { return p.PublicPort + '→' + p.PrivatePort + (p.Type === 'udp' ? '/udp' : ''); }).join(', ');
|
|
@@ -177,20 +230,23 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
177
230
|
el('td', { class: 'muted', text: ago(c.Created) }),
|
|
178
231
|
el('td', null, [acts]),
|
|
179
232
|
]);
|
|
180
|
-
}
|
|
233
|
+
}
|
|
234
|
+
var focused = document.activeElement && document.activeElement.classList.contains('search');
|
|
181
235
|
view.textContent = '';
|
|
182
236
|
view.appendChild(bar);
|
|
237
|
+
if (focused) { search.focus(); search.setSelectionRange(search.value.length, search.value.length); }
|
|
183
238
|
view.appendChild(table(['Name', 'Image', 'Status', 'Ports', 'Created', ''], rows, 'No containers yet. Start one with: thinknsh run -d nginx:alpine'));
|
|
184
239
|
var sel = list.filter(function (c) { return c.Id === state.selected; })[0];
|
|
185
240
|
if (sel) detail(sel); else stopDetail();
|
|
186
241
|
});
|
|
187
242
|
}
|
|
188
243
|
|
|
189
|
-
var live = { id: null, logs: null, stats: null };
|
|
244
|
+
var live = { id: null, logs: null, stats: null, term: null };
|
|
190
245
|
function stopDetail() {
|
|
191
246
|
if (live.logs) live.logs.abort();
|
|
192
247
|
if (live.stats) clearInterval(live.stats);
|
|
193
|
-
live
|
|
248
|
+
if (live.term) live.term.close();
|
|
249
|
+
live = { id: null, logs: null, stats: null, term: null };
|
|
194
250
|
}
|
|
195
251
|
|
|
196
252
|
function detail(c) {
|
|
@@ -200,10 +256,21 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
200
256
|
stopDetail();
|
|
201
257
|
live.id = key;
|
|
202
258
|
var logs = el('pre', { class: 'logs', text: '' });
|
|
259
|
+
var term = el('pre', { class: 'term', tabindex: '0', hidden: true });
|
|
260
|
+
var logFilter = el('input', { class: 'search', type: 'search', placeholder: 'Filter log lines', oninput: function () { logs.applyFilter(logFilter.value); } });
|
|
261
|
+
var seg = el('div', { class: 'seg' });
|
|
262
|
+
var showPane = function (pane) {
|
|
263
|
+
state.pane = pane;
|
|
264
|
+
logs.hidden = pane !== 'logs'; logFilter.hidden = pane !== 'logs'; term.hidden = pane !== 'terminal';
|
|
265
|
+
Array.prototype.forEach.call(seg.children, function (b) { b.className = b.getAttribute('data-pane') === pane ? 'on' : ''; });
|
|
266
|
+
if (pane === 'terminal') { if (!live.term) live.term = openTerminal(c.Id, term); term.focus(); }
|
|
267
|
+
};
|
|
268
|
+
seg.appendChild(el('button', { text: 'Logs', 'data-pane': 'logs', onclick: function () { showPane('logs'); } }));
|
|
269
|
+
if (running) seg.appendChild(el('button', { text: 'Terminal', 'data-pane': 'terminal', onclick: function () { showPane('terminal'); } }));
|
|
203
270
|
var cpu = el('div', { class: 'v', text: '–' }), cpuBar = el('i'), mem = el('div', { class: 'v', text: '–' }), memBar = el('i'), net = el('div', { class: 'v', text: '–' }), pids = el('div', { class: 'v', text: '–' });
|
|
204
271
|
var info = el('dl');
|
|
205
272
|
var box = el('div', { class: 'detail', id: 'detail' }, [
|
|
206
|
-
el('div', null, [el('div', { class: 'toolbar' }, [
|
|
273
|
+
el('div', null, [el('div', { class: 'toolbar' }, [seg, logFilter, el('span', { class: 'muted', text: running ? 'following' : 'last 500 lines' })]), logs, term]),
|
|
207
274
|
el('div', { class: 'stats' }, [
|
|
208
275
|
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'CPU' }), cpu, el('div', { class: 'bar' }, [cpuBar])]),
|
|
209
276
|
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'Memory' }), mem, el('div', { class: 'bar' }, [memBar])]),
|
|
@@ -213,6 +280,7 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
213
280
|
]),
|
|
214
281
|
]);
|
|
215
282
|
view.appendChild(box);
|
|
283
|
+
showPane(running && state.pane === 'terminal' ? 'terminal' : 'logs');
|
|
216
284
|
|
|
217
285
|
api('GET', '/containers/' + c.Id + '/json').then(function (d) {
|
|
218
286
|
var add = function (k, v) { if (v) { info.appendChild(el('dt', { text: k })); info.appendChild(el('dd', { class: 'mono', text: v })); } };
|
|
@@ -250,12 +318,26 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
250
318
|
function streamLogs(id, tty, follow, pre) {
|
|
251
319
|
var ctl = new AbortController();
|
|
252
320
|
live.logs = ctl;
|
|
321
|
+
// every line is kept (up to 20000) so the filter can search all of them
|
|
322
|
+
var all = [''], filter = '';
|
|
323
|
+
var render = function () {
|
|
324
|
+
var q = filter.toLowerCase();
|
|
325
|
+
pre.textContent = all.filter(function (l) { return l.toLowerCase().indexOf(q) !== -1; }).join('\n');
|
|
326
|
+
};
|
|
327
|
+
pre.applyFilter = function (q) { filter = q; if (q) render(); else pre.textContent = all.join('\n'); pre.scrollTop = pre.scrollHeight; };
|
|
253
328
|
fetch('/api/containers/' + id + '/logs?stdout=1&stderr=1&tail=500' + (follow ? '&follow=1' : ''), { headers: { 'x-tnc-ui': token }, signal: ctl.signal }).then(function (r) {
|
|
254
329
|
if (!r.ok || !r.body) return;
|
|
255
330
|
var reader = r.body.getReader(), dec = new TextDecoder(), buf = new Uint8Array(0);
|
|
331
|
+
var pendingRender = null;
|
|
256
332
|
var push = function (bytes) {
|
|
257
333
|
var atEnd = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 20;
|
|
258
|
-
|
|
334
|
+
var text = dec.decode(bytes, { stream: true });
|
|
335
|
+
var parts = text.split('\n');
|
|
336
|
+
all[all.length - 1] += parts[0];
|
|
337
|
+
for (var i = 1; i < parts.length; i++) all.push(parts[i]);
|
|
338
|
+
if (all.length > 20000) all.splice(0, all.length - 20000);
|
|
339
|
+
if (filter) { if (!pendingRender) pendingRender = setTimeout(function () { pendingRender = null; render(); }, 200); return; }
|
|
340
|
+
pre.appendChild(document.createTextNode(text));
|
|
259
341
|
if (pre.childNodes.length > 4000) pre.removeChild(pre.firstChild);
|
|
260
342
|
if (atEnd) pre.scrollTop = pre.scrollHeight;
|
|
261
343
|
};
|
|
@@ -277,6 +359,138 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
277
359
|
}).catch(function () {});
|
|
278
360
|
}
|
|
279
361
|
|
|
362
|
+
/*
|
|
363
|
+
Terminal: a shell (TTY exec) over a WebSocket. A small line renderer:
|
|
364
|
+
escape sequences are dropped, \r and backspace edit the current line.
|
|
365
|
+
Enough for shells and command output; full-screen programs need ssh/exec.
|
|
366
|
+
*/
|
|
367
|
+
function openTerminal(id, pre) {
|
|
368
|
+
var sock = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ui/terminal?id=' + encodeURIComponent(id), ['tnc-ui.' + token]);
|
|
369
|
+
sock.binaryType = 'arraybuffer';
|
|
370
|
+
var dec = new TextDecoder(), lines = [''], col = 0;
|
|
371
|
+
var draw = function () {
|
|
372
|
+
pre.textContent = lines.join('\n');
|
|
373
|
+
pre.scrollTop = pre.scrollHeight;
|
|
374
|
+
};
|
|
375
|
+
var feed = function (text) {
|
|
376
|
+
text = text.replace(/\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(\x07|\x1b\\)|\x1b[()][0-9A-Za-z]|\x1b[=>78]/g, '');
|
|
377
|
+
for (var i = 0; i < text.length; i++) {
|
|
378
|
+
var ch = text[i], cur = lines[lines.length - 1];
|
|
379
|
+
if (ch === '\n') { lines.push(''); col = 0; }
|
|
380
|
+
else if (ch === '\r') { col = 0; }
|
|
381
|
+
else if (ch === '\b') { if (col > 0) col--; }
|
|
382
|
+
else if (ch === '\x07') { }
|
|
383
|
+
else { lines[lines.length - 1] = cur.slice(0, col) + ch + cur.slice(col + 1); col++; }
|
|
384
|
+
}
|
|
385
|
+
if (lines.length > 5000) lines.splice(0, lines.length - 5000);
|
|
386
|
+
draw();
|
|
387
|
+
};
|
|
388
|
+
var resize = function () {
|
|
389
|
+
var w = Math.max(20, Math.floor(pre.clientWidth / 7.6)), h = Math.max(5, Math.floor(pre.clientHeight / 18));
|
|
390
|
+
if (sock.readyState === 1) sock.send(JSON.stringify({ resize: [h, w] }));
|
|
391
|
+
};
|
|
392
|
+
sock.onopen = function () { resize(); };
|
|
393
|
+
sock.onmessage = function (e) { feed(typeof e.data === 'string' ? e.data : dec.decode(new Uint8Array(e.data), { stream: true })); };
|
|
394
|
+
sock.onclose = function () { feed('\r\n[session ended]'); };
|
|
395
|
+
sock.onerror = function () { toast('terminal: cannot connect'); };
|
|
396
|
+
var KEYS = { Enter: '\r', Backspace: '\x7f', Tab: '\t', Escape: '\x1b', ArrowUp: '\x1b[A', ArrowDown: '\x1b[B', ArrowRight: '\x1b[C', ArrowLeft: '\x1b[D', Home: '\x1b[H', End: '\x1b[F', Delete: '\x1b[3~' };
|
|
397
|
+
pre.onkeydown = function (e) {
|
|
398
|
+
if (sock.readyState !== 1) return;
|
|
399
|
+
var data = null;
|
|
400
|
+
if (e.ctrlKey && !e.metaKey && e.key.length === 1) {
|
|
401
|
+
var code = e.key.toUpperCase().charCodeAt(0);
|
|
402
|
+
if (code >= 64 && code <= 95) data = String.fromCharCode(code - 64);
|
|
403
|
+
} else if (KEYS[e.key]) data = KEYS[e.key];
|
|
404
|
+
else if (e.key.length === 1 && !e.metaKey) data = e.key;
|
|
405
|
+
if (data !== null) { e.preventDefault(); sock.send(data); }
|
|
406
|
+
};
|
|
407
|
+
pre.onpaste = function (e) { e.preventDefault(); var t = (e.clipboardData || window.clipboardData).getData('text'); if (t && sock.readyState === 1) sock.send(t); };
|
|
408
|
+
return { close: function () { try { sock.close(); } catch (e) {} } };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ── settings ── */
|
|
412
|
+
function settings() {
|
|
413
|
+
return ui('GET', '/settings').then(function (d) {
|
|
414
|
+
var s = d.settings, saved = d.saved || {}, p = s.proxy || {};
|
|
415
|
+
var lines = function (a) { return (a || []).join('\n'); };
|
|
416
|
+
var split = function (t) { return t.split(/[\n,]/).map(function (x) { return x.trim(); }).filter(Boolean); };
|
|
417
|
+
var field = function (label, input, hint) {
|
|
418
|
+
return el('label', { class: 'field' }, [el('span', { text: label }), input, hint ? el('div', { class: 'hint', text: hint }) : null]);
|
|
419
|
+
};
|
|
420
|
+
var input = function (value, type, attrs) { var a = attrs || {}; a.type = type || 'text'; a.value = value == null ? '' : String(value); return el('input', a); };
|
|
421
|
+
var area = function (value, placeholder) { var t = el('textarea', { placeholder: placeholder }); t.value = value; return t; };
|
|
422
|
+
var f = {
|
|
423
|
+
autostart: el('input', { type: 'checkbox', checked: d.autostart.enabled }),
|
|
424
|
+
cpus: input(s.cpus, 'number', { min: '1' }), memoryGB: input((s.memoryMB / 1024).toFixed(1).replace(/\.0$/, ''), 'number', { min: '0.5', step: '0.5' }),
|
|
425
|
+
diskGB: input(s.diskGB, 'number', { min: '8' }), shares: area(lines(saved.shares), '/Users/me/projects'),
|
|
426
|
+
http: input(p.http, 'text', { placeholder: 'http://proxy.corp:3128' }), https: input(p.https, 'text', { placeholder: 'http://proxy.corp:3128' }),
|
|
427
|
+
noProxy: input(p.noProxy, 'text', { placeholder: '.corp, 10.0.0.0/8' }), proxyContainers: el('input', { type: 'checkbox', checked: p.containers !== false }),
|
|
428
|
+
caCerts: area(lines(saved.caCerts), '/Users/me/corp-root-ca.pem'),
|
|
429
|
+
mirrors: area(lines(saved.registryMirrors), 'https://mirror.gcr.io'), insecure: area(lines(saved.insecureRegistries), 'registry.lan:5000'),
|
|
430
|
+
credentialStore: el('select', null, [el('option', { value: 'auto', text: 'System keychain (' + d.credentials + ')' }), el('option', { value: 'file', text: 'auth.json file (mode 0600)' })]),
|
|
431
|
+
};
|
|
432
|
+
f.credentialStore.value = s.credentialStore || 'auto';
|
|
433
|
+
var save = function () {
|
|
434
|
+
var patch = {
|
|
435
|
+
autostart: f.autostart.checked,
|
|
436
|
+
cpus: Number(f.cpus.value), memoryMB: Math.round(Number(f.memoryGB.value) * 1024), diskGB: Number(f.diskGB.value),
|
|
437
|
+
shares: split(f.shares.value),
|
|
438
|
+
proxy: { http: f.http.value.trim() || undefined, https: f.https.value.trim() || undefined, noProxy: f.noProxy.value.trim() || undefined, containers: f.proxyContainers.checked ? undefined : false },
|
|
439
|
+
caCerts: split(f.caCerts.value), registryMirrors: split(f.mirrors.value), insecureRegistries: split(f.insecure.value),
|
|
440
|
+
credentialStore: f.credentialStore.value,
|
|
441
|
+
};
|
|
442
|
+
ui('PUT', '/settings', patch).then(function () { toast('Saved. VM settings apply after "Restart Linux VM"; proxy and CA after "Restart dock".', true); refresh(); }).catch(function (e) { toast(e.message); });
|
|
443
|
+
};
|
|
444
|
+
var form = el('div', { class: 'form' }, [
|
|
445
|
+
el('fieldset', null, [el('legend', { text: 'General' }),
|
|
446
|
+
field('Start at login', f.autostart, 'Starts the engine (and the menu-bar app, if installed) when you log in.'),
|
|
447
|
+
field('Logins stored in', f.credentialStore)]),
|
|
448
|
+
d.platform === 'darwin' ? el('fieldset', null, [el('legend', { text: 'Linux VM' }),
|
|
449
|
+
field('CPUs', f.cpus), field('Memory (GB)', f.memoryGB, 'A ceiling: macOS backs only the memory containers use.'),
|
|
450
|
+
field('Disk size (GB)', f.diskGB, 'Used when the data disk is created.'),
|
|
451
|
+
field('Shared folders', f.shares, 'Extra folders for bind mounts, besides your home, /tmp and /Volumes. One per line.')]) : null,
|
|
452
|
+
el('fieldset', null, [el('legend', { text: 'Proxy' }),
|
|
453
|
+
field('HTTP proxy', f.http), field('HTTPS proxy', f.https), field('Bypass for', f.noProxy, 'Comma-separated hosts or domains.'),
|
|
454
|
+
field('Pass to containers', f.proxyContainers, 'Builds and new containers get the proxy variables (localhost becomes host.tnc.internal).'),
|
|
455
|
+
d.nodeProxy ? null : el('div', { class: 'hint', text: 'This Node version ignores proxy settings: install Node 22.21+ or 24.5+.' })]),
|
|
456
|
+
el('fieldset', null, [el('legend', { text: 'Registries' }),
|
|
457
|
+
field('Mirrors', f.mirrors, 'Tried first for images from the public default registry. One URL per line.'),
|
|
458
|
+
field('Insecure (plain HTTP)', f.insecure, 'host or host:port, one per line. localhost needs no entry.'),
|
|
459
|
+
field('Extra CA certificates', f.caCerts, 'PEM files your company signs registry certificates with. One path per line.')]),
|
|
460
|
+
el('div', { class: 'actions' }, [
|
|
461
|
+
btn('Save', save),
|
|
462
|
+
btn('Restart dock', function () { if (confirm('Restart dock? Containers keep running in the Linux VM.')) ui('POST', '/engine/restart').then(function (r) { toast(r.message, true); }).catch(function (e) { toast(e.message); }); }),
|
|
463
|
+
d.platform !== 'linux' ? btn('Restart Linux VM', function () { if (confirm('Restart the Linux VM? Running containers stop.')) ui('POST', '/vm/restart').then(function (r) { toast(r.message, true); }).catch(function (e) { toast(e.message); }); }) : null,
|
|
464
|
+
]),
|
|
465
|
+
]);
|
|
466
|
+
view.textContent = '';
|
|
467
|
+
view.appendChild(form);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ── troubleshoot ── */
|
|
472
|
+
function troubleshoot() {
|
|
473
|
+
return ui('GET', '/log').then(function (d) {
|
|
474
|
+
var pre = el('pre', { class: 'logs', text: d.log || '(empty)' });
|
|
475
|
+
var confirmBox = el('input', { class: 'search', type: 'text', placeholder: 'type reset to confirm' });
|
|
476
|
+
view.textContent = '';
|
|
477
|
+
view.appendChild(el('div', { class: 'grid' }, [
|
|
478
|
+
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'Diagnostics' }), el('p', { class: 'muted', text: 'A .tar.gz with versions, settings, the container list and recent logs, for a bug report. No logins or secrets.' }),
|
|
479
|
+
btn('Create bundle', function () { ui('POST', '/diagnose').then(function (r) { toast('Saved: ' + r.file, true); }).catch(function (e) { toast(e.message); }); })]),
|
|
480
|
+
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'Clean up' }), el('p', { class: 'muted', text: 'Stopped containers, unused networks, untagged images. Volumes are kept.' }),
|
|
481
|
+
btn('Clean up', function () {
|
|
482
|
+
if (!confirm('Remove stopped containers, unused networks and untagged images?')) return;
|
|
483
|
+
Promise.all([api('POST', '/containers/prune'), api('POST', '/networks/prune'), api('POST', '/images/prune')]).then(function () { toast('Cleaned up', true); }).catch(function (e) { toast(e.message); });
|
|
484
|
+
})]),
|
|
485
|
+
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'Reset to factory defaults' }), el('p', { class: 'muted', text: 'Deletes ALL containers, images, volumes and the VM disk. Settings and logins are kept.' }),
|
|
486
|
+
el('div', { class: 'actions' }, [confirmBox, btn('Reset', function () { ui('POST', '/reset', { confirm: confirmBox.value.trim() }).then(function (r) { toast(r.message, true); }).catch(function (e) { toast(e.message); }); }, { danger: true })])]),
|
|
487
|
+
]));
|
|
488
|
+
view.appendChild(el('div', { class: 'toolbar', style: 'margin-top:14px' }, [el('strong', { text: 'Engine log' }), el('span', { class: 'muted', text: 'last 400 lines' })]));
|
|
489
|
+
view.appendChild(pre);
|
|
490
|
+
pre.scrollTop = pre.scrollHeight;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
280
494
|
/* ── images, volumes, networks, system ── */
|
|
281
495
|
function images() {
|
|
282
496
|
return api('GET', '/images/json').then(function (list) {
|
|
@@ -353,9 +567,11 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
353
567
|
});
|
|
354
568
|
}
|
|
355
569
|
|
|
356
|
-
var VIEWS = { Containers: containers, Images: images, Volumes: volumes, Networks: networks, System: system };
|
|
570
|
+
var VIEWS = { Containers: containers, Images: images, Volumes: volumes, Networks: networks, System: system, Settings: settings, Troubleshoot: troubleshoot };
|
|
357
571
|
var pending = null;
|
|
358
|
-
function refresh() {
|
|
572
|
+
function refresh(fromEvent) {
|
|
573
|
+
// forms are not redrawn by engine events (that would drop what is being typed)
|
|
574
|
+
if (fromEvent === true && (state.tab === 'Settings' || state.tab === 'Troubleshoot')) return;
|
|
359
575
|
clearTimeout(pending);
|
|
360
576
|
pending = setTimeout(function () { VIEWS[state.tab]().catch(function (e) { toast(e.message); }); }, 60);
|
|
361
577
|
}
|
|
@@ -365,7 +581,7 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
365
581
|
fetch('/api/events', { headers: { 'x-tnc-ui': token } }).then(function (r) {
|
|
366
582
|
if (!r.ok || !r.body) throw new Error('events');
|
|
367
583
|
var reader = r.body.getReader();
|
|
368
|
-
var pump = function () { return reader.read().then(function (res) { if (res.done) throw new Error('ended'); refresh(); return pump(); }); };
|
|
584
|
+
var pump = function () { return reader.read().then(function (res) { if (res.done) throw new Error('ended'); refresh(true); return pump(); }); };
|
|
369
585
|
return pump();
|
|
370
586
|
}).catch(function () { setTimeout(events, 3000); });
|
|
371
587
|
}
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
5
|
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
6
|
*/
|
|
7
|
-
"use strict";const http=require("http"),crypto=require("crypto"),{PAGE}=require("./page"),ID="[^/]+",ALLOWED=[["GET",/^\/.*$/],["POST",new RegExp(`^/containers/${ID}/(start|stop|restart|kill|pause|unpause)$`)],["POST",/^\/(containers|images|volumes|networks)\/prune$/],["DELETE",new RegExp(`^/(containers|volumes|networks)/${ID}$`)],["DELETE",/^\/images\/.+$/]],BLOCKED=/\/(archive|export|attach|exec)$|^\/exec\/|^\/images\/(.+\/)?get$|^\/(session|grpc)$/;function allowed(e,t){return!BLOCKED.test(t)&&ALLOWED.some(([n,
|
|
7
|
+
"use strict";const fs=require("fs"),path=require("path"),http=require("http"),crypto=require("crypto"),{spawn}=require("child_process"),{PAGE}=require("./page"),ws=require("../api/websocket"),CLI=path.join(__dirname,"..","..","..","bin","thinknsh.js"),ID="[^/]+",ALLOWED=[["GET",/^\/.*$/],["POST",new RegExp(`^/containers/${ID}/(start|stop|restart|kill|pause|unpause)$`)],["POST",/^\/(containers|images|volumes|networks)\/prune$/],["DELETE",new RegExp(`^/(containers|volumes|networks)/${ID}$`)],["DELETE",/^\/images\/.+$/]],BLOCKED=/\/(archive|export|attach|exec)$|^\/exec\/|^\/images\/(.+\/)?get$|^\/(session|grpc)$/;function allowed(e,t){return!BLOCKED.test(t)&&ALLOWED.some(([n,r])=>n===e&&r.test(t))}function sameToken(e,t){const n=Buffer.from(String(e||"")),r=Buffer.from(String(t||""));return n.length===r.length&&crypto.timingSafeEqual(n,r)}function readJson(e,t=1<<20){return new Promise((n,r)=>{let o=0;const s=[];e.on("data",n=>{o+=n.length,o>t?(r(new Error("body too large")),e.destroy()):s.push(n)}),e.on("end",()=>{try{n(s.length?JSON.parse(Buffer.concat(s).toString("utf8")):{})}catch(e){r(new Error("invalid JSON"))}}),e.on("error",r)})}function engine(e,t,n,r){return new Promise((o,s)=>{const a=void 0===r?null:Buffer.from(JSON.stringify(r)),i=http.request({socketPath:e,method:t,path:n,headers:a?{"Content-Type":"application/json","Content-Length":a.length}:{}},e=>{const t=[];e.on("data",e=>t.push(e)),e.on("end",()=>{const n=Buffer.concat(t).toString("utf8");let r=n;try{r=n?JSON.parse(n):null}catch(e){}o({status:e.statusCode,body:r})})});i.on("error",s),a&&i.write(a),i.end()})}function settingsView(){const e=require("../config");return{settings:e.effective(),saved:e.readConfig(),defaults:e.defaults(),autostart:require("../setup/autostart").status(),credentials:require("../image/keychain").describe(),nodeProxy:e.nodeHasEnvProxy(),platform:process.platform}}function cli(e){const t=spawn(process.execPath,[CLI,...e],{stdio:"ignore",detached:!0,windowsHide:!0,env:{...process.env,FORCE_COLOR:"0"}});t.on("error",()=>{}),t.unref()}async function uiRoute(e,t,n,{socketPath:r,send:o}){const s=n.pathname.slice(3),a=e.method;if("GET"===a&&"/settings"===s)return o(200,settingsView());if("PUT"===a&&"/settings"===s){const t=require("../config"),n=await readJson(e);if(void 0!==n.autostart){const e=require("../setup/autostart");n.autostart?e.enable():e.disable()}const{autostart:r,...s}=n;return Object.keys(s).length&&t.writeConfig(t.applyPatch(t.readConfig(),s)),o(200,settingsView())}if("POST"===a&&"/engine/restart"===s)return cli(["dock","stop"]),setTimeout(()=>cli(["dock","start"]),1500),o(202,{message:"restarting dock"});if("POST"===a&&"/vm/restart"===s)return cli(["dock","vm-restart","--force"]),o(202,{message:"restarting the Linux VM"});if("POST"===a&&"/diagnose"===s)return o(200,{file:await require("../setup/diagnose").diagnose()});if("POST"===a&&"/reset"===s)return"reset"!==(await readJson(e)).confirm?o(400,{message:"type reset to confirm"}):(cli(["dock","reset","--yes"]),o(202,{message:"resetting dock"}));if("GET"===a&&"/log"===s){const{dockPaths:e}=require("../paths"),{redact:t}=require("../setup/diagnose");let n="";try{n=fs.readFileSync(e().logFile,"utf8").split("\n").slice(-400).join("\n")}catch(e){}return o(200,{log:t(n)})}return o(404,{message:"not found"})}async function terminal(e,t,n,{socketPath:r,id:o}){const s=await engine(r,"POST",`/containers/${encodeURIComponent(o)}/exec`,{AttachStdin:!0,AttachStdout:!0,AttachStderr:!0,Tty:!0,Env:["TERM=xterm-256color"],Cmd:["sh","-c","command -v bash >/dev/null 2>&1 && exec bash -l || exec sh -l"]});if(201!==s.status||!s.body?.Id)throw new Error(s.body?.message||`exec failed: HTTP ${s.status}`);const a=s.body.Id,i=ws.accept(e,t,n,{protocol:e.headers["sec-websocket-protocol"].split(",")[0].trim()}),c=Buffer.from(JSON.stringify({Detach:!1,Tty:!0})),d=http.request({socketPath:r,method:"POST",path:`/exec/${a}/start`,headers:{"Content-Type":"application/json","Content-Length":c.length,Connection:"Upgrade",Upgrade:"tcp"}});d.on("upgrade",(e,t,n)=>{n?.length&&i.send(n,!0),t.on("data",e=>i.send(e,!0)),t.on("close",()=>i.close()),t.on("error",()=>i.close()),i.on("message",(e,n)=>{if(!n&&123===e[0])try{const t=JSON.parse(e.toString("utf8")).resize;return void(Array.isArray(t)&&engine(r,"POST",`/exec/${a}/resize?h=${0|Number(t[0])}&w=${0|Number(t[1])}`).catch(()=>{}))}catch(e){}t.write(e)}),i.on("close",()=>t.destroy())}),d.on("error",e=>{i.send(`\r\n[dock: ${e.message}]\r\n`),i.close()}),d.end(c)}function startUi({socketPath:e,port:t=0,token:n=crypto.randomBytes(24).toString("hex")}={}){let r=null;const o=e=>null!==r&&(e===`127.0.0.1:${r}`||e===`localhost:${r}`),s=http.createServer((t,r)=>{const s=new URL(t.url,"http://ui"),a=(e,t)=>{r.writeHead(e,{"Content-Type":"application/json","Cache-Control":"no-store"}),r.end(JSON.stringify({message:t}))};if(!o(t.headers.host))return a(421,"wrong Host");const i={"Cache-Control":"no-store","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","Referrer-Policy":"no-referrer"};if("GET"===t.method&&("/"===s.pathname||"/index.html"===s.pathname))return r.writeHead(200,{...i,"Content-Type":"text/html; charset=utf-8","Content-Security-Policy":"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"}),r.end(PAGE);if(s.pathname.startsWith("/ui/")){if(!sameToken(t.headers["x-tnc-ui"],n))return a(403,"missing or wrong session token");const o=(e,t)=>(r.writeHead(e,{...i,"Content-Type":"application/json"}),r.end(JSON.stringify(t)),!0);return void uiRoute(t,r,s,{socketPath:e,send:o}).catch(e=>{r.headersSent?r.end():o(e.statusCode||500,{message:e.message})})}if(!s.pathname.startsWith("/api/"))return a(404,"not found");if(!sameToken(t.headers["x-tnc-ui"],n))return a(403,"missing or wrong session token");const c=s.pathname.slice(4);if(!allowed(t.method,c))return a(403,`${t.method} ${c} is not available from the dashboard`);const d=http.request({socketPath:e,method:t.method,path:c+s.search,headers:{"Content-Type":t.headers["content-type"]||"application/json",...t.headers["content-length"]?{"Content-Length":t.headers["content-length"]}:{}}},e=>{r.writeHead(e.statusCode,{...i,"Content-Type":e.headers["content-type"]||"application/octet-stream"}),e.pipe(r),r.on("close",()=>e.destroy())});d.on("error",e=>{r.headersSent?r.end():a(502,`dock is not reachable: ${e.message}`)}),t.pipe(d)});return s.on("upgrade",(t,s,a)=>{s.on("error",()=>{});const i=(e,t)=>{s.end(`HTTP/1.1 ${e} ${t}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n`)},c=new URL(t.url,"http://ui");if(!o(t.headers.host)||(d=t.headers.origin,null===r||d!==`http://127.0.0.1:${r}`&&d!==`http://localhost:${r}`))return i(403,"Forbidden");var d;if("/ui/terminal"!==c.pathname||!ws.isUpgrade(t))return i(404,"Not Found");if(!String(t.headers["sec-websocket-protocol"]||"").split(",").map(e=>e.trim()).some(e=>e.startsWith("tnc-ui.")&&sameToken(e.slice(7),n)))return i(403,"Forbidden");const u=c.searchParams.get("id")||"";if(!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(u))return i(400,"Bad Request");terminal(t,s,a,{socketPath:e,id:u}).catch(e=>i(502,String(e.message).replace(/[\r\n]/g," ").slice(0,200)))}),new Promise((e,o)=>{s.once("error",o),s.listen(t,"127.0.0.1",()=>{r=s.address().port,e({port:r,token:n,url:`http://127.0.0.1:${r}/#t=${n}`,close:()=>new Promise(e=>{s.closeAllConnections?.(),s.close(()=>e())})})})})}module.exports={startUi,allowed};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkncollab-shell",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"description": "Lightweight MicroVM Sandbox & Zero-Daemon Dev Runner: Fast MicroVM Containers, Native Microservices, Live Database Snapshots, and End-to-End Encrypted State Sync",
|
|
5
5
|
"main": "dist/src/core/shell.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license ThinkNCollab Enterprise & Open Core Ecosystem
|
|
3
|
-
* Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
|
|
4
|
-
* PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
|
|
5
|
-
* UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
|
|
6
|
-
*/
|
|
7
|
-
"use strict";const fs=require("fs"),os=require("os"),path=require("path"),{spawnSync}=require("child_process"),APP=path.join(os.homedir(),"Applications","TNC Dock.app"),BUNDLE_ID="com.thinkncollab.dock.menubar",LOGIN_AGENT=path.join(os.homedir(),"Library","LaunchAgents",`${BUNDLE_ID}.plist`),EXE="tnc-menubar",SOURCE=path.join(__dirname,"..","..","..","native","tnc-menubar","main.swift");function infoPlist(n){return`<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>\n <key>CFBundleName</key><string>TNC Dock</string>\n <key>CFBundleDisplayName</key><string>TNC Dock</string>\n <key>CFBundleExecutable</key><string>${EXE}</string>\n <key>CFBundlePackageType</key><string>APPL</string>\n <key>CFBundleShortVersionString</key><string>${n}</string>\n <key>CFBundleVersion</key><string>${n}</string>\n <key>LSMinimumSystemVersion</key><string>13.0</string>\n <key>LSUIElement</key><true/>\n <key>NSHighResolutionCapable</key><true/>\n</dict>\n</plist>\n`}function binary(n,e){const i=path.join(path.dirname(n),"bin",EXE);if(fs.existsSync(i))return i;if(fs.existsSync(SOURCE)&&0===spawnSync("swiftc",["--version"],{stdio:"ignore"}).status){const i=path.join(n,"build",EXE);fs.mkdirSync(path.dirname(i),{recursive:!0}),e("compiling the menu bar app (swiftc)…");const s=spawnSync("swiftc",["-swift-version","5","-O","-framework","AppKit","-o",i,SOURCE],{encoding:"utf8"});if(0!==s.status)throw new Error(`swiftc failed:\n${(s.stderr||s.stdout).trim().slice(-2e3)}`);return i}throw new Error("the menu bar app is not installed; run thinknsh dock setup (a newer runtime bundle has it)")}function quitApp(){spawnSync("pkill",["-f",`${APP}/Contents/MacOS/${EXE}`],{stdio:"ignore"})}function app({off:n=!1,home:e,socket:i,cli:s,version:t="0",log:r=()=>{}}){if("darwin"!==process.platform)throw new Error("the menu bar app is for macOS; on other systems use thinknsh dock ui");if(n){quitApp();const n=fs.existsSync(APP);return fs.rmSync(APP,{recursive:!0,force:!0}),fs.rmSync(LOGIN_AGENT,{force:!0}),[n?`removed ${APP}`:"TNC Dock was not installed"]}const o=binary(e,r);quitApp();const c=path.join(APP,"Contents");fs.rmSync(APP,{recursive:!0,force:!0}),fs.mkdirSync(path.join(c,"MacOS"),{recursive:!0}),fs.mkdirSync(path.join(c,"Resources"),{recursive:!0}),fs.writeFileSync(path.join(c,"Info.plist"),infoPlist(t)),fs.copyFileSync(o,path.join(c,"MacOS",EXE)),fs.chmodSync(path.join(c,"MacOS",EXE),493),fs.writeFileSync(path.join(c,"Resources","config.json"),JSON.stringify({node:process.execPath,cli:fs.realpathSync(s),socket:i},null,2)+"\n");const a=spawnSync("codesign",["-s","-","--force","--deep",APP],{encoding:"utf8"});return 0!==a.status&&r(`codesign: ${(a.stderr||"").trim()}`),spawnSync("open",[APP]),[`TNC Dock is in the menu bar (${APP})`,'its menu: containers, ports, logs, dock start/stop, the dashboard, and "Open TNC Dock at login"',`uses node ${process.execPath}; run this again if you move or upgrade node`]}module.exports={app,APP};
|