thinkncollab-shell 2.0.0-beta.1.1 → 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.
Files changed (51) hide show
  1. package/Readme.md +41 -10
  2. package/dist/native/tnc-tray/build-macos.sh +54 -0
  3. package/dist/native/tnc-tray/main.swift +201 -0
  4. package/dist/native/tnc-tray/tray.ps1 +136 -0
  5. package/dist/src/commands/dock/containers.js +1 -1
  6. package/dist/src/commands/dock/dock.js +1 -1
  7. package/dist/src/dock/api/routes.js +1 -1
  8. package/dist/src/dock/api/server.js +1 -1
  9. package/dist/src/dock/api/websocket.js +7 -0
  10. package/dist/src/dock/api/wire.js +1 -1
  11. package/dist/src/dock/build/builder.js +1 -1
  12. package/dist/src/dock/build/buildkit.js +1 -1
  13. package/dist/src/dock/build/cache.js +7 -0
  14. package/dist/src/dock/build/sshagent.js +7 -0
  15. package/dist/src/dock/cli/api.js +1 -1
  16. package/dist/src/dock/cli/commands.js +1 -1
  17. package/dist/src/dock/cli/compose.js +1 -1
  18. package/dist/src/dock/cli/trust.js +7 -0
  19. package/dist/src/dock/compat/cases.js +1 -1
  20. package/dist/src/dock/config.js +1 -1
  21. package/dist/src/dock/daemon-main.js +1 -1
  22. package/dist/src/dock/daemon.js +1 -1
  23. package/dist/src/dock/engine.js +1 -1
  24. package/dist/src/dock/image/auth.js +1 -1
  25. package/dist/src/dock/image/credhelpers.js +7 -0
  26. package/dist/src/dock/image/keychain.js +7 -0
  27. package/dist/src/dock/image/registry.js +1 -1
  28. package/dist/src/dock/image/scan.js +7 -0
  29. package/dist/src/dock/image/sign.js +7 -0
  30. package/dist/src/dock/linux/host.js +1 -1
  31. package/dist/src/dock/paths.js +1 -1
  32. package/dist/src/dock/runtime/cdi.js +7 -0
  33. package/dist/src/dock/runtime/fsops.js +1 -1
  34. package/dist/src/dock/runtime/network.js +1 -1
  35. package/dist/src/dock/runtime/overlaydns.js +7 -0
  36. package/dist/src/dock/runtime/ports.js +1 -1
  37. package/dist/src/dock/runtime/rootfs.js +1 -1
  38. package/dist/src/dock/runtime/spec.js +1 -1
  39. package/dist/src/dock/runtime/stats.js +1 -1
  40. package/dist/src/dock/runtime/userns.js +7 -0
  41. package/dist/src/dock/runtime/volumes.js +1 -1
  42. package/dist/src/dock/setup/autostart.js +1 -1
  43. package/dist/src/dock/setup/diagnose.js +7 -0
  44. package/dist/src/dock/setup/index.js +1 -1
  45. package/dist/src/dock/state/store.js +1 -1
  46. package/dist/src/dock/tray/agent.js +7 -0
  47. package/dist/src/dock/tray/install.js +7 -0
  48. package/dist/src/dock/ui/page.js +228 -15
  49. package/dist/src/dock/ui/server.js +1 -1
  50. package/package.json +1 -1
  51. package/dist/src/dock/setup/app.js +0 -7
@@ -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
- return fetch('/api' + path, { method: method, headers: { 'x-tnc-ui': token || '' } }).then(function (r) {
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
- var rows = list.map(function (c) {
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 = { id: null, logs: null, stats: null };
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' }, [el('strong', { text: 'Logs' }), el('span', { class: 'muted', text: running ? 'following' : 'last 500 lines' })]), logs]),
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
- pre.appendChild(document.createTextNode(dec.decode(bytes, { stream: true })));
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,o])=>n===e&&o.test(t))}function sameToken(e,t){const n=Buffer.from(String(e||"")),o=Buffer.from(String(t||""));return n.length===o.length&&crypto.timingSafeEqual(n,o)}function startUi({socketPath:e,port:t=0,token:n=crypto.randomBytes(24).toString("hex")}={}){let o=null;const r=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(i=t.headers.host,null===o||i!==`127.0.0.1:${o}`&&i!==`localhost:${o}`)return a(421,"wrong Host");var i;const c={"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,{...c,"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("/api/"))return a(404,"not found");if(!sameToken(t.headers["x-tnc-ui"],n))return a(403,"missing or wrong session token");const l=s.pathname.slice(4);if(!allowed(t.method,l))return a(403,`${t.method} ${l} is not available from the dashboard`);const p=http.request({socketPath:e,method:t.method,path:l+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,{...c,"Content-Type":e.headers["content-type"]||"application/octet-stream"}),e.pipe(r),r.on("close",()=>e.destroy())});p.on("error",e=>{r.headersSent?r.end():a(502,`dock is not reachable: ${e.message}`)}),t.pipe(p)});return new Promise((e,s)=>{r.once("error",s),r.listen(t,"127.0.0.1",()=>{o=r.address().port,e({port:o,token:n,url:`http://127.0.0.1:${o}/#t=${n}`,close:()=>new Promise(e=>{r.closeAllConnections?.(),r.close(()=>e())})})})})}module.exports={startUi,allowed};
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.1.1",
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};