thinkncollab-shell 2.0.0-beta.2 → 2.0.0-beta.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/Readme.md +41 -10
- package/dist/native/tnc-tray/build-macos.sh +54 -0
- package/dist/native/tnc-tray/main.swift +201 -0
- package/dist/native/tnc-tray/tray.ps1 +136 -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 +228 -15
- 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
|
@@ -68,6 +68,23 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
68
68
|
#toast { position: fixed; right: 16px; bottom: 16px; max-width: 420px; display: grid; gap: 8px; z-index: 5; }
|
|
69
69
|
#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
70
|
#locked { padding: 60px 20px; text-align: center; color: var(--muted); }
|
|
71
|
+
#toast div.ok { border-color: var(--ok); }
|
|
72
|
+
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; }
|
|
73
|
+
input.search { min-width: 220px; }
|
|
74
|
+
tr.group td { background: var(--chip); font-weight: 600; cursor: default; }
|
|
75
|
+
.form { display: grid; gap: 18px; max-width: 760px; }
|
|
76
|
+
.form fieldset { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin: 0; display: grid; gap: 10px; }
|
|
77
|
+
.form legend { font-weight: 600; padding: 0 6px; }
|
|
78
|
+
.form .field { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 10px; align-items: center; }
|
|
79
|
+
.form .field > span { color: var(--muted); font-size: 13px; }
|
|
80
|
+
.form textarea { min-height: 54px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
|
|
81
|
+
.form .hint { color: var(--muted); font-size: 12px; grid-column: 2; margin-top: -6px; }
|
|
82
|
+
@media (max-width: 640px) { .form .field { grid-template-columns: 1fr; } .form .hint { grid-column: 1; } }
|
|
83
|
+
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; }
|
|
84
|
+
pre.term:focus { box-shadow: 0 0 0 2px var(--accent); }
|
|
85
|
+
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
|
|
86
|
+
.seg button { border: 0; background: var(--panel); color: var(--muted); font: inherit; font-size: 12.5px; padding: 4px 10px; cursor: pointer; }
|
|
87
|
+
.seg button.on { background: var(--chip); color: var(--text); }
|
|
71
88
|
</style>
|
|
72
89
|
</head>
|
|
73
90
|
<body>
|
|
@@ -86,6 +103,7 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
86
103
|
(function () {
|
|
87
104
|
'use strict';
|
|
88
105
|
var token = (location.hash.match(/t=([0-9a-f]+)/) || [])[1] || sessionStorage.getItem('tnc-ui-token');
|
|
106
|
+
var startTab = (location.hash.match(/tab=([a-z]+)/) || [])[1];
|
|
89
107
|
if (token) { sessionStorage.setItem('tnc-ui-token', token); history.replaceState(null, '', location.pathname); }
|
|
90
108
|
|
|
91
109
|
var root = document.documentElement;
|
|
@@ -108,13 +126,15 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
108
126
|
(kids || []).forEach(function (c) { if (c != null) n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); });
|
|
109
127
|
return n;
|
|
110
128
|
}
|
|
111
|
-
function toast(msg) {
|
|
112
|
-
var box = el('div', { text: msg });
|
|
129
|
+
function toast(msg, ok) {
|
|
130
|
+
var box = el('div', { text: msg, class: ok ? 'ok' : '' });
|
|
113
131
|
document.getElementById('toast').appendChild(box);
|
|
114
132
|
setTimeout(function () { box.remove(); }, 6000);
|
|
115
133
|
}
|
|
116
|
-
function api(method, path) {
|
|
117
|
-
|
|
134
|
+
function api(method, path, body, prefix) {
|
|
135
|
+
var headers = { 'x-tnc-ui': token || '' };
|
|
136
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
137
|
+
return fetch((prefix || '/api') + path, { method: method, headers: headers, body: body === undefined ? undefined : JSON.stringify(body) }).then(function (r) {
|
|
118
138
|
if (r.status === 204 || r.status === 304) return null;
|
|
119
139
|
return r.text().then(function (t) {
|
|
120
140
|
var body; try { body = t ? JSON.parse(t) : null; } catch (e) { body = t; }
|
|
@@ -132,8 +152,11 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
132
152
|
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
153
|
var short = function (id) { return String(id || '').replace(/^sha256:/, '').slice(0, 12); };
|
|
134
154
|
|
|
135
|
-
var TABS = ['Containers', 'Images', 'Volumes', 'Networks', 'System'];
|
|
136
|
-
var state = { tab: 'Containers', all: true, selected: null };
|
|
155
|
+
var TABS = ['Containers', 'Images', 'Volumes', 'Networks', 'System', 'Settings', 'Troubleshoot'];
|
|
156
|
+
var state = { tab: 'Containers', all: true, selected: null, filter: '', pane: 'logs' };
|
|
157
|
+
TABS.forEach(function (t) { if (startTab && t.toLowerCase() === startTab) state.tab = t; });
|
|
158
|
+
var ui = function (method, path, body) { return api(method, path, body, '/ui'); };
|
|
159
|
+
var PROJECT = 'tnc.compose.project';
|
|
137
160
|
var view = document.getElementById('view');
|
|
138
161
|
|
|
139
162
|
function renderTabs() {
|
|
@@ -153,12 +176,39 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
153
176
|
/* ── containers ── */
|
|
154
177
|
function containers() {
|
|
155
178
|
return api('GET', '/containers/json' + (state.all ? '?all=1' : '')).then(function (list) {
|
|
179
|
+
var q = state.filter.toLowerCase();
|
|
180
|
+
if (q) list = list.filter(function (c) { return ((c.Names || []).join(' ') + ' ' + c.Image + ' ' + ((c.Labels || {})[PROJECT] || '')).toLowerCase().indexOf(q) !== -1; });
|
|
181
|
+
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
182
|
var bar = el('div', { class: 'toolbar' }, [
|
|
183
|
+
search,
|
|
157
184
|
el('label', null, [el('input', { type: 'checkbox', checked: state.all, onchange: function (e) { state.all = e.target.checked; refresh(); } }), 'show stopped']),
|
|
158
185
|
el('span', { class: 'spacer' }),
|
|
159
186
|
btn('Remove stopped', function () { act('POST', '/containers/prune', 'Remove every stopped container?'); }),
|
|
160
187
|
]);
|
|
161
|
-
|
|
188
|
+
// compose apps first (grouped by project), then single containers
|
|
189
|
+
list.sort(function (a, b) {
|
|
190
|
+
var pa = (a.Labels || {})[PROJECT] || '\uffff', pb = (b.Labels || {})[PROJECT] || '\uffff';
|
|
191
|
+
return pa < pb ? -1 : pa > pb ? 1 : (a.Names[0] < b.Names[0] ? -1 : 1);
|
|
192
|
+
});
|
|
193
|
+
var rows = [];
|
|
194
|
+
var lastGroup = null;
|
|
195
|
+
list.forEach(function (c) {
|
|
196
|
+
var project = (c.Labels || {})[PROJECT] || null;
|
|
197
|
+
if (project !== lastGroup) {
|
|
198
|
+
lastGroup = project;
|
|
199
|
+
var members = list.filter(function (x) { return ((x.Labels || {})[PROJECT] || null) === project; });
|
|
200
|
+
var up = members.filter(function (x) { return x.State === 'running'; });
|
|
201
|
+
rows.push(el('tr', { class: 'group' }, [
|
|
202
|
+
el('td', { colspan: '5', text: project ? project + ' · ' + up.length + '/' + members.length + ' running' : 'Other containers' }),
|
|
203
|
+
el('td', null, [project ? el('div', { class: 'actions' }, [
|
|
204
|
+
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 }),
|
|
205
|
+
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 }),
|
|
206
|
+
]) : null]),
|
|
207
|
+
]));
|
|
208
|
+
}
|
|
209
|
+
rows.push(containerRow(c));
|
|
210
|
+
});
|
|
211
|
+
function containerRow(c) {
|
|
162
212
|
var name = (c.Names && c.Names[0] || '').replace(/^\//, '');
|
|
163
213
|
var st = c.State;
|
|
164
214
|
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 +227,23 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
177
227
|
el('td', { class: 'muted', text: ago(c.Created) }),
|
|
178
228
|
el('td', null, [acts]),
|
|
179
229
|
]);
|
|
180
|
-
}
|
|
230
|
+
}
|
|
231
|
+
var focused = document.activeElement && document.activeElement.classList.contains('search');
|
|
181
232
|
view.textContent = '';
|
|
182
233
|
view.appendChild(bar);
|
|
234
|
+
if (focused) { search.focus(); search.setSelectionRange(search.value.length, search.value.length); }
|
|
183
235
|
view.appendChild(table(['Name', 'Image', 'Status', 'Ports', 'Created', ''], rows, 'No containers yet. Start one with: thinknsh run -d nginx:alpine'));
|
|
184
236
|
var sel = list.filter(function (c) { return c.Id === state.selected; })[0];
|
|
185
237
|
if (sel) detail(sel); else stopDetail();
|
|
186
238
|
});
|
|
187
239
|
}
|
|
188
240
|
|
|
189
|
-
var live = { id: null, logs: null, stats: null };
|
|
241
|
+
var live = { id: null, logs: null, stats: null, term: null };
|
|
190
242
|
function stopDetail() {
|
|
191
243
|
if (live.logs) live.logs.abort();
|
|
192
244
|
if (live.stats) clearInterval(live.stats);
|
|
193
|
-
live
|
|
245
|
+
if (live.term) live.term.close();
|
|
246
|
+
live = { id: null, logs: null, stats: null, term: null };
|
|
194
247
|
}
|
|
195
248
|
|
|
196
249
|
function detail(c) {
|
|
@@ -200,10 +253,21 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
200
253
|
stopDetail();
|
|
201
254
|
live.id = key;
|
|
202
255
|
var logs = el('pre', { class: 'logs', text: '' });
|
|
256
|
+
var term = el('pre', { class: 'term', tabindex: '0', hidden: true });
|
|
257
|
+
var logFilter = el('input', { class: 'search', type: 'search', placeholder: 'Filter log lines', oninput: function () { logs.applyFilter(logFilter.value); } });
|
|
258
|
+
var seg = el('div', { class: 'seg' });
|
|
259
|
+
var showPane = function (pane) {
|
|
260
|
+
state.pane = pane;
|
|
261
|
+
logs.hidden = pane !== 'logs'; logFilter.hidden = pane !== 'logs'; term.hidden = pane !== 'terminal';
|
|
262
|
+
Array.prototype.forEach.call(seg.children, function (b) { b.className = b.getAttribute('data-pane') === pane ? 'on' : ''; });
|
|
263
|
+
if (pane === 'terminal') { if (!live.term) live.term = openTerminal(c.Id, term); term.focus(); }
|
|
264
|
+
};
|
|
265
|
+
seg.appendChild(el('button', { text: 'Logs', 'data-pane': 'logs', onclick: function () { showPane('logs'); } }));
|
|
266
|
+
if (running) seg.appendChild(el('button', { text: 'Terminal', 'data-pane': 'terminal', onclick: function () { showPane('terminal'); } }));
|
|
203
267
|
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
268
|
var info = el('dl');
|
|
205
269
|
var box = el('div', { class: 'detail', id: 'detail' }, [
|
|
206
|
-
el('div', null, [el('div', { class: 'toolbar' }, [
|
|
270
|
+
el('div', null, [el('div', { class: 'toolbar' }, [seg, logFilter, el('span', { class: 'muted', text: running ? 'following' : 'last 500 lines' })]), logs, term]),
|
|
207
271
|
el('div', { class: 'stats' }, [
|
|
208
272
|
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'CPU' }), cpu, el('div', { class: 'bar' }, [cpuBar])]),
|
|
209
273
|
el('div', { class: 'stat' }, [el('div', { class: 'k', text: 'Memory' }), mem, el('div', { class: 'bar' }, [memBar])]),
|
|
@@ -213,6 +277,7 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
213
277
|
]),
|
|
214
278
|
]);
|
|
215
279
|
view.appendChild(box);
|
|
280
|
+
showPane(running && state.pane === 'terminal' ? 'terminal' : 'logs');
|
|
216
281
|
|
|
217
282
|
api('GET', '/containers/' + c.Id + '/json').then(function (d) {
|
|
218
283
|
var add = function (k, v) { if (v) { info.appendChild(el('dt', { text: k })); info.appendChild(el('dd', { class: 'mono', text: v })); } };
|
|
@@ -250,12 +315,26 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
250
315
|
function streamLogs(id, tty, follow, pre) {
|
|
251
316
|
var ctl = new AbortController();
|
|
252
317
|
live.logs = ctl;
|
|
318
|
+
// every line is kept (up to 20000) so the filter can search all of them
|
|
319
|
+
var all = [''], filter = '';
|
|
320
|
+
var render = function () {
|
|
321
|
+
var q = filter.toLowerCase();
|
|
322
|
+
pre.textContent = all.filter(function (l) { return l.toLowerCase().indexOf(q) !== -1; }).join('\n');
|
|
323
|
+
};
|
|
324
|
+
pre.applyFilter = function (q) { filter = q; if (q) render(); else pre.textContent = all.join('\n'); pre.scrollTop = pre.scrollHeight; };
|
|
253
325
|
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
326
|
if (!r.ok || !r.body) return;
|
|
255
327
|
var reader = r.body.getReader(), dec = new TextDecoder(), buf = new Uint8Array(0);
|
|
328
|
+
var pendingRender = null;
|
|
256
329
|
var push = function (bytes) {
|
|
257
330
|
var atEnd = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 20;
|
|
258
|
-
|
|
331
|
+
var text = dec.decode(bytes, { stream: true });
|
|
332
|
+
var parts = text.split('\n');
|
|
333
|
+
all[all.length - 1] += parts[0];
|
|
334
|
+
for (var i = 1; i < parts.length; i++) all.push(parts[i]);
|
|
335
|
+
if (all.length > 20000) all.splice(0, all.length - 20000);
|
|
336
|
+
if (filter) { if (!pendingRender) pendingRender = setTimeout(function () { pendingRender = null; render(); }, 200); return; }
|
|
337
|
+
pre.appendChild(document.createTextNode(text));
|
|
259
338
|
if (pre.childNodes.length > 4000) pre.removeChild(pre.firstChild);
|
|
260
339
|
if (atEnd) pre.scrollTop = pre.scrollHeight;
|
|
261
340
|
};
|
|
@@ -277,6 +356,138 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
277
356
|
}).catch(function () {});
|
|
278
357
|
}
|
|
279
358
|
|
|
359
|
+
/*
|
|
360
|
+
Terminal: a shell (TTY exec) over a WebSocket. A small line renderer:
|
|
361
|
+
escape sequences are dropped, \r and backspace edit the current line.
|
|
362
|
+
Enough for shells and command output; full-screen programs need ssh/exec.
|
|
363
|
+
*/
|
|
364
|
+
function openTerminal(id, pre) {
|
|
365
|
+
var sock = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ui/terminal?id=' + encodeURIComponent(id), ['tnc-ui.' + token]);
|
|
366
|
+
sock.binaryType = 'arraybuffer';
|
|
367
|
+
var dec = new TextDecoder(), lines = [''], col = 0;
|
|
368
|
+
var draw = function () {
|
|
369
|
+
pre.textContent = lines.join('\n');
|
|
370
|
+
pre.scrollTop = pre.scrollHeight;
|
|
371
|
+
};
|
|
372
|
+
var feed = function (text) {
|
|
373
|
+
text = text.replace(/\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(\x07|\x1b\\)|\x1b[()][0-9A-Za-z]|\x1b[=>78]/g, '');
|
|
374
|
+
for (var i = 0; i < text.length; i++) {
|
|
375
|
+
var ch = text[i], cur = lines[lines.length - 1];
|
|
376
|
+
if (ch === '\n') { lines.push(''); col = 0; }
|
|
377
|
+
else if (ch === '\r') { col = 0; }
|
|
378
|
+
else if (ch === '\b') { if (col > 0) col--; }
|
|
379
|
+
else if (ch === '\x07') { }
|
|
380
|
+
else { lines[lines.length - 1] = cur.slice(0, col) + ch + cur.slice(col + 1); col++; }
|
|
381
|
+
}
|
|
382
|
+
if (lines.length > 5000) lines.splice(0, lines.length - 5000);
|
|
383
|
+
draw();
|
|
384
|
+
};
|
|
385
|
+
var resize = function () {
|
|
386
|
+
var w = Math.max(20, Math.floor(pre.clientWidth / 7.6)), h = Math.max(5, Math.floor(pre.clientHeight / 18));
|
|
387
|
+
if (sock.readyState === 1) sock.send(JSON.stringify({ resize: [h, w] }));
|
|
388
|
+
};
|
|
389
|
+
sock.onopen = function () { resize(); };
|
|
390
|
+
sock.onmessage = function (e) { feed(typeof e.data === 'string' ? e.data : dec.decode(new Uint8Array(e.data), { stream: true })); };
|
|
391
|
+
sock.onclose = function () { feed('\r\n[session ended]'); };
|
|
392
|
+
sock.onerror = function () { toast('terminal: cannot connect'); };
|
|
393
|
+
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~' };
|
|
394
|
+
pre.onkeydown = function (e) {
|
|
395
|
+
if (sock.readyState !== 1) return;
|
|
396
|
+
var data = null;
|
|
397
|
+
if (e.ctrlKey && !e.metaKey && e.key.length === 1) {
|
|
398
|
+
var code = e.key.toUpperCase().charCodeAt(0);
|
|
399
|
+
if (code >= 64 && code <= 95) data = String.fromCharCode(code - 64);
|
|
400
|
+
} else if (KEYS[e.key]) data = KEYS[e.key];
|
|
401
|
+
else if (e.key.length === 1 && !e.metaKey) data = e.key;
|
|
402
|
+
if (data !== null) { e.preventDefault(); sock.send(data); }
|
|
403
|
+
};
|
|
404
|
+
pre.onpaste = function (e) { e.preventDefault(); var t = (e.clipboardData || window.clipboardData).getData('text'); if (t && sock.readyState === 1) sock.send(t); };
|
|
405
|
+
return { close: function () { try { sock.close(); } catch (e) {} } };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* ── settings ── */
|
|
409
|
+
function settings() {
|
|
410
|
+
return ui('GET', '/settings').then(function (d) {
|
|
411
|
+
var s = d.settings, saved = d.saved || {}, p = s.proxy || {};
|
|
412
|
+
var lines = function (a) { return (a || []).join('\n'); };
|
|
413
|
+
var split = function (t) { return t.split(/[\n,]/).map(function (x) { return x.trim(); }).filter(Boolean); };
|
|
414
|
+
var field = function (label, input, hint) {
|
|
415
|
+
return el('label', { class: 'field' }, [el('span', { text: label }), input, hint ? el('div', { class: 'hint', text: hint }) : null]);
|
|
416
|
+
};
|
|
417
|
+
var input = function (value, type, attrs) { var a = attrs || {}; a.type = type || 'text'; a.value = value == null ? '' : String(value); return el('input', a); };
|
|
418
|
+
var area = function (value, placeholder) { var t = el('textarea', { placeholder: placeholder }); t.value = value; return t; };
|
|
419
|
+
var f = {
|
|
420
|
+
autostart: el('input', { type: 'checkbox', checked: d.autostart.enabled }),
|
|
421
|
+
cpus: input(s.cpus, 'number', { min: '1' }), memoryGB: input((s.memoryMB / 1024).toFixed(1).replace(/\.0$/, ''), 'number', { min: '0.5', step: '0.5' }),
|
|
422
|
+
diskGB: input(s.diskGB, 'number', { min: '8' }), shares: area(lines(saved.shares), '/Users/me/projects'),
|
|
423
|
+
http: input(p.http, 'text', { placeholder: 'http://proxy.corp:3128' }), https: input(p.https, 'text', { placeholder: 'http://proxy.corp:3128' }),
|
|
424
|
+
noProxy: input(p.noProxy, 'text', { placeholder: '.corp, 10.0.0.0/8' }), proxyContainers: el('input', { type: 'checkbox', checked: p.containers !== false }),
|
|
425
|
+
caCerts: area(lines(saved.caCerts), '/Users/me/corp-root-ca.pem'),
|
|
426
|
+
mirrors: area(lines(saved.registryMirrors), 'https://mirror.gcr.io'), insecure: area(lines(saved.insecureRegistries), 'registry.lan:5000'),
|
|
427
|
+
credentialStore: el('select', null, [el('option', { value: 'auto', text: 'System keychain (' + d.credentials + ')' }), el('option', { value: 'file', text: 'auth.json file (mode 0600)' })]),
|
|
428
|
+
};
|
|
429
|
+
f.credentialStore.value = s.credentialStore || 'auto';
|
|
430
|
+
var save = function () {
|
|
431
|
+
var patch = {
|
|
432
|
+
autostart: f.autostart.checked,
|
|
433
|
+
cpus: Number(f.cpus.value), memoryMB: Math.round(Number(f.memoryGB.value) * 1024), diskGB: Number(f.diskGB.value),
|
|
434
|
+
shares: split(f.shares.value),
|
|
435
|
+
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 },
|
|
436
|
+
caCerts: split(f.caCerts.value), registryMirrors: split(f.mirrors.value), insecureRegistries: split(f.insecure.value),
|
|
437
|
+
credentialStore: f.credentialStore.value,
|
|
438
|
+
};
|
|
439
|
+
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); });
|
|
440
|
+
};
|
|
441
|
+
var form = el('div', { class: 'form' }, [
|
|
442
|
+
el('fieldset', null, [el('legend', { text: 'General' }),
|
|
443
|
+
field('Start at login', f.autostart, 'Starts the engine (and the menu-bar app, if installed) when you log in.'),
|
|
444
|
+
field('Logins stored in', f.credentialStore)]),
|
|
445
|
+
d.platform === 'darwin' ? el('fieldset', null, [el('legend', { text: 'Linux VM' }),
|
|
446
|
+
field('CPUs', f.cpus), field('Memory (GB)', f.memoryGB, 'A ceiling: macOS backs only the memory containers use.'),
|
|
447
|
+
field('Disk size (GB)', f.diskGB, 'Used when the data disk is created.'),
|
|
448
|
+
field('Shared folders', f.shares, 'Extra folders for bind mounts, besides your home, /tmp and /Volumes. One per line.')]) : null,
|
|
449
|
+
el('fieldset', null, [el('legend', { text: 'Proxy' }),
|
|
450
|
+
field('HTTP proxy', f.http), field('HTTPS proxy', f.https), field('Bypass for', f.noProxy, 'Comma-separated hosts or domains.'),
|
|
451
|
+
field('Pass to containers', f.proxyContainers, 'Builds and new containers get the proxy variables (localhost becomes host.tnc.internal).'),
|
|
452
|
+
d.nodeProxy ? null : el('div', { class: 'hint', text: 'This Node version ignores proxy settings: install Node 22.21+ or 24.5+.' })]),
|
|
453
|
+
el('fieldset', null, [el('legend', { text: 'Registries' }),
|
|
454
|
+
field('Mirrors', f.mirrors, 'Tried first for images from the public default registry. One URL per line.'),
|
|
455
|
+
field('Insecure (plain HTTP)', f.insecure, 'host or host:port, one per line. localhost needs no entry.'),
|
|
456
|
+
field('Extra CA certificates', f.caCerts, 'PEM files your company signs registry certificates with. One path per line.')]),
|
|
457
|
+
el('div', { class: 'actions' }, [
|
|
458
|
+
btn('Save', save),
|
|
459
|
+
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); }); }),
|
|
460
|
+
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,
|
|
461
|
+
]),
|
|
462
|
+
]);
|
|
463
|
+
view.textContent = '';
|
|
464
|
+
view.appendChild(form);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ── troubleshoot ── */
|
|
469
|
+
function troubleshoot() {
|
|
470
|
+
return ui('GET', '/log').then(function (d) {
|
|
471
|
+
var pre = el('pre', { class: 'logs', text: d.log || '(empty)' });
|
|
472
|
+
var confirmBox = el('input', { class: 'search', type: 'text', placeholder: 'type reset to confirm' });
|
|
473
|
+
view.textContent = '';
|
|
474
|
+
view.appendChild(el('div', { class: 'grid' }, [
|
|
475
|
+
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.' }),
|
|
476
|
+
btn('Create bundle', function () { ui('POST', '/diagnose').then(function (r) { toast('Saved: ' + r.file, true); }).catch(function (e) { toast(e.message); }); })]),
|
|
477
|
+
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.' }),
|
|
478
|
+
btn('Clean up', function () {
|
|
479
|
+
if (!confirm('Remove stopped containers, unused networks and untagged images?')) return;
|
|
480
|
+
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); });
|
|
481
|
+
})]),
|
|
482
|
+
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.' }),
|
|
483
|
+
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 })])]),
|
|
484
|
+
]));
|
|
485
|
+
view.appendChild(el('div', { class: 'toolbar', style: 'margin-top:14px' }, [el('strong', { text: 'Engine log' }), el('span', { class: 'muted', text: 'last 400 lines' })]));
|
|
486
|
+
view.appendChild(pre);
|
|
487
|
+
pre.scrollTop = pre.scrollHeight;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
280
491
|
/* ── images, volumes, networks, system ── */
|
|
281
492
|
function images() {
|
|
282
493
|
return api('GET', '/images/json').then(function (list) {
|
|
@@ -353,9 +564,11 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
353
564
|
});
|
|
354
565
|
}
|
|
355
566
|
|
|
356
|
-
var VIEWS = { Containers: containers, Images: images, Volumes: volumes, Networks: networks, System: system };
|
|
567
|
+
var VIEWS = { Containers: containers, Images: images, Volumes: volumes, Networks: networks, System: system, Settings: settings, Troubleshoot: troubleshoot };
|
|
357
568
|
var pending = null;
|
|
358
|
-
function refresh() {
|
|
569
|
+
function refresh(fromEvent) {
|
|
570
|
+
// forms are not redrawn by engine events (that would drop what is being typed)
|
|
571
|
+
if (fromEvent === true && (state.tab === 'Settings' || state.tab === 'Troubleshoot')) return;
|
|
359
572
|
clearTimeout(pending);
|
|
360
573
|
pending = setTimeout(function () { VIEWS[state.tab]().catch(function (e) { toast(e.message); }); }, 60);
|
|
361
574
|
}
|
|
@@ -365,7 +578,7 @@ dl { display: grid; grid-template-columns: 110px minmax(0,1fr); gap: 4px 10px; m
|
|
|
365
578
|
fetch('/api/events', { headers: { 'x-tnc-ui': token } }).then(function (r) {
|
|
366
579
|
if (!r.ok || !r.body) throw new Error('events');
|
|
367
580
|
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(); }); };
|
|
581
|
+
var pump = function () { return reader.read().then(function (res) { if (res.done) throw new Error('ended'); refresh(true); return pump(); }); };
|
|
369
582
|
return pump();
|
|
370
583
|
}).catch(function () { setTimeout(events, 3000); });
|
|
371
584
|
}
|
|
@@ -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.3",
|
|
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};
|